如何優化 App 的啓動耗時?

原文:iOS面試題大全

iOS 的 App 啓動主要分爲以下步驟:

  • 打開 App,系統內核進行初始化跳轉到 dyld 執行。這個過程包括這些步驟:1)分配虛擬內存空間;2)fork 進程;3)加載 MachO (自身所有的可執行 MachO 文件的集合)到進程空間;4)加載動態鏈接器 dyld 並將控制權交給 dyld 處理。在這個過程中內核會產生 ASLR(Address space layout randomization) 隨機數值,這個值用於加載的 MachO 起始地址在內存中的偏移,隨機的地址可防止 MachO 代碼掃描並被 hack,提升安全性。通過 ASLR 雖然可隨機化各內存區基地址,但無法將程序內的代碼段和數據段隨機化,如果繞過(bypass) ASLR 依然可進行篡改,就需要結合 PIE(Position Independent Executable) 共同使用。與之相似的還有 PIC(Position Independent Code),位置無關代碼,作用於共享庫代碼。PIE/PIC 技術需要在編譯階段開啓。顧名思義,PIC 可將程序代碼裝載到任意地址,這樣就內部的指針不能靠固定的絕對地址訪問,而通過相對地址指令如 adrp 來獲取代碼和數據。
  • 進入 dyld 動態鏈接器,它負責將一個 App 處理爲一個可運行的狀態,包含:
  • 加載 MachO 的依賴庫(這些依賴庫也是 MachO 格式的文件)。dyld 從可執行 MachO 文件的依賴開始, 遞歸加載所有依賴的動態庫。 動態庫包括:iOS 中用到的所有系統動態庫:加載 OC runtime 方法的 libobjc,系統級別的 libSystem(例如 libdispatch(GCD) 和 libsystem_blocks(Block));其他 App 自己的動態庫。根據 Apple 的描述,大部分 App 所加載的庫在 100~400 個。不過 iOS 系統庫已經被特殊優化過,如提前加入共享緩存,提前做好地址修正等。
  • Fix-ups(地址修正),包括 rebasing 和 binding 等。ASLR + PIE 技術增強了程序的安全性,使得依賴固定地址進行攻擊的方法失效,但也增加了程序自身的複雜度,MachO 文件的 rebase 和 bind info 等部分以及啓動時的 fix-ups 地址修正階段就是配合它而產生的。
  • ObjC 環境配置。經過了 MachO 程序和依賴庫的加載以及地址修正之後,dyld 所做的大部分事情已經完成了。在這一階段,dyld 開始對主程序的依賴庫進行初始化工作,而初始化的執行部分會回調到依賴庫內部執行,如 ObjC 的運行時環境所在的 libobjc.A.dylib 以及 libdispatch.dylib 等。ObjC Setup 的過程,主要是對 ObjC 數據進行關聯註冊:1)dyld 將主程序 MachO 基址指針和包含的 ObjC 相關類信息傳遞到 libobjc;2)ObjC Runtime 從 __DATA 段中獲取 ObjC 類信息,由於 ObjC 是動態語言,可以通過類名獲取其實例,所以 Runtime 維護了一個映射所有類的全局類名錶。當加載的數據包含了類的定義,類的名字就需要註冊到全局表中;3)獲取 protocol、category 等類相關屬性並與對應類進行關聯;4)ObjC 的調用都是基於 selector 的,所以需要對 selector 全局唯一性進行處理。以上步驟由 dyld 啓動 libSystem.dylib 統一對基礎庫進行調用執行,這裏面就包含了 libobjc 的 Runtime,同時 Runtime 會在 dyld 綁定回調,當 dyld 處理完相關數據後就會調用 ObjC Runtime 執行 Setup 工作。
  • 執行各模塊初始化器。從這一步就開始接近上(業務)層:1)通過 ObjC Runtime 在 dyld 註冊的通知,當 MachO 鏡像準備完畢後,dyld 會回調到 ObjC 中執行 +load() 方法,包括以下步驟:a)獲取所有 non-lazy class 列表;b)按繼承以及 category 的順序將類排入待加載列表;c)對待加載列表中的類進行方法判斷並調用 +load() 方法。2)執行 C/C++ 初始化構造器,如通過 attribute((constructor)) 註解的函數。3)如果包含 C++,則 dyld 同樣會回調到 libc++ 庫中對全局靜態變量、隱式初始化等進行調用。
  • 查找並跳轉到 main() 函數入口。到了最後,dyld 回到 Load command,找到 LC_MAIN,拿到 entryoff 再加上 MachO 在內存的加載首地址(首地址就是內核傳來的 slide 偏移)就得到了 main() 的入口地址,從而進入我們顯式的程序邏輯。

進入 main() -> UIApplicationMain -> 初始化回調 -> 顯示UI。

iOS 的 App 啓動時長大概可以這樣計算:

t(App 總啓動時間) = t1(main 調用之前的加載時間) + t2(main 調用之後的加載時間)。

t1 = 系統 dylib(動態鏈接庫)和自身 App 可執行文件的加載。

t2 = main 方法執行之後到 AppDelegate 類中的 application:didFinishLaunchingWithOptions:方法執行結束前這段時間,主要是構建第一個界面,並完成渲染展示。

在 t1 階段加快 App 啓動的建議:

  • 儘量使用靜態庫,減少動態庫的使用,動態鏈接比較耗時。
  • 如果要用動態庫,儘量將多個 dylib 動態庫合併成一個。
  • 儘量避免對系統庫使用 optional linking,如果 App 用到的系統庫在你所有支持的系統版本上都有,就設置爲 required,因爲 optional 會有些額外的檢查。
  • 減少 Objective-C Class、Selector、Category 的數量。可以合併或者刪減一些 OC 類。
  • 刪減一些無用的靜態變量,刪減沒有被調用到或者已經廢棄的方法。
  • 將不必須在 +load 中做的事情儘量挪到 +initialize 中,+initialize 是在第一次初始化這個類之前被調用,+load 在加載類的時候就被調用。儘量將 +load 裏的代碼延後調用。
  • 儘量不要用 C++ 虛函數,創建虛函數表有開銷。
  • 不要使用 __atribute__((constructor)) 將方法顯式標記爲初始化器,而是讓初始化方法調用時才執行。比如使用 dispatch_once()pthread_once() std::once()
  • 在初始化方法中不調用 dlopen()dlopen() 有性能和死鎖的可能性。
  • 在初始化方法中不創建線程。

在 t2 階段加快 App 啓動的建議:

  • 儘量不要使用 xib/storyboard,而是用純代碼作爲首頁 UI。
  • 如果要用 xib/storyboard,不要在 xib/storyboard 中存放太多的視圖。
  • application:didFinishLaunchingWithOptions: 裏的任務儘量延遲加載或懶加載。
  • 不要在 NSUserDefaults 中存放太多的數據,NSUserDefaults 是一個 plist 文件,plist 文件被反序列化一次。
  • 避免在啓動時打印過多的 log。
  • 少用 NSLog,因爲每一次 NSLog 的調用都會創建一個新的 NSCalendar 實例。
  • 每一段 SQLite 語句都是一個段被編譯的程序,調用 sqlite3_prepare 將編譯 SQLite 查詢到字節碼,使用 sqlite_bind_int 綁定參數到 SQLite 語句。
  • 爲了防止使用 GCD 創建過多的線程,解決方法是創建串行隊列, 或者使用帶有最大併發數限制的 NSOperationQueue。
  • 線程安全:UIKit只能在主線程執行,除了 UIGraphics、UIBezierPath 之外,UIImage、CG、CA、Foundation 都不能從兩個線程同時訪問。
  • 不要在主線程執行磁盤、網絡、Lock 或者 dispatch_sync、發送消息給其他線程等操作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章