iOS端啟動速度優化
應用啟動流程
iOS應用的啟動可分為pre-main階段和main()階段,其中系統做的事情依次是:
1. pre-main階段
1.1. 載入應用的可執行檔案
1.2. 載入動態連結庫載入器dyld(dynamic loader)
1.3. dyld遞迴載入應用所有依賴的dylib(dynamic library 動態連結庫)
對於pre-main階段,Apple提供了一種測量各階段的耗時的方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環境變數DYLD_PRINT_STATISTICS 設為1 ,設定好後把程式跑起來,控制檯會有如下輸出:
2. main()階段
2.1. dyld呼叫main()
2.2. 呼叫UIApplicationMain()
2.3. 呼叫applicationWillFinishLaunching
2.4. 呼叫didFinishLaunchingWithOptions
對於main()階段,主要是測量main()函式開始執行到didFinishLaunchingWithOptions執行結束的耗時,就需要自己插入程式碼到工程中了。先在main()函式裡用變數StartTime記錄當前時間:
CFAbsoluteTime StartTime;
int main(int argc, char * argv[]) {
StartTime = CFAbsoluteTimeGetCurrent();
再在AppDelegate.m檔案中用extern宣告全域性變數StartTime
extern CFAbsoluteTime StartTime;
最後在didFinishLaunchingWithOptions裡,再獲取一下當前時間,與StartTime的差值即是main()階段執行耗時。
double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);
pre-main階段的優化
要對pre-main階段的耗時做優化,需要再學習下dyld載入的過程,根據Apple在WWDC上的介紹,dyld的載入主要分為4步:
1. Load dylibs
這一階段dyld會分析應用依賴的dylib,找到其mach-o檔案,開啟和讀取這些檔案並驗證其有效性,接著會找到程式碼簽名註冊到核心,最後對dylib的每一個segment呼叫mmap()。
一般情況下,iOS應用會載入100-400個dylibs,其中大部分是系統庫,這部分dylib的載入系統已經做了優化。
所以,依賴的dylib越少越好。在這一步,我們可以做的優化有:
儘量不使用內嵌(embedded)的dylib,載入內嵌dylib效能開銷較大
合併已有的dylib和使用靜態庫(static archives),減少dylib的使用個數
懶載入dylib,但是要注意dlopen()可能造成一些問題,且實際上懶載入做的工作會更多
2. Rebase/Bind
在dylib的載入過程中,系統為了安全考慮,引入了ASLR(Address Space Layout Randomization)技術和程式碼簽名。由於ASLR的存在,映象(Image,包括可執行檔案、dylib和bundle)會在隨機的地址上載入,和之前指標指向的地址(preferred_address)會有一個偏差(slide),dyld需要修正這個偏差,來指向正確的地址。
Rebase在前,Bind在後,Rebase做的是將映象讀入記憶體,修正映象內部的指標,效能消耗主要在IO。Bind做的是查詢符號表,設定指向映象外部的指標,效能消耗主要在CPU計算。
所以,指標數量越少越好。在這一步,我們可以做的優化有:
減少ObjC類(class)、方法(selector)、分類(category)的數量
減少C++虛擬函式的的數量(建立虛擬函式表有開銷)
使用Swift structs(內部做了優化,符號數量更少)
3. Objc setup
大部分ObjC初始化工作已經在Rebase/Bind階段做完了,這一步dyld會註冊所有宣告過的ObjC類,將分類插入到類的方法列表裡,再檢查每個selector的唯一性。
在這一步倒沒什麼優化可做的,Rebase/Bind階段優化好了,這一步的耗時也會減少。
4. Initializers
到了這一階段,dyld開始執行程式的初始化函式,呼叫每個Objc類和分類的+load方法,呼叫C/C++ 中的構造器函式(用attribute((constructor))修飾的函式),和建立非基本型別的C++靜態全域性變數。Initializers階段執行完後,dyld開始呼叫main()函式。
在這一步,我們可以做的優化有:
少在類的+load方法裡做事情,儘量把這些事情推遲到+initiailize
減少構造器函式個數,在構造器函式裡少做些事情
減少C++靜態全域性變數的個數
main()階段的優化
這一階段的優化主要是減少didFinishLaunchingWithOptions方法裡的工作,在didFinishLaunchingWithOptions方法裡,我們會建立應用的window,指定其rootViewController,呼叫window的makeKeyAndVisible方法讓其可見。由於業務需要,我們會初始化各個二方/三方庫,設定系統UI風格,檢查是否需要顯示引導頁、是否需要登入、是否有新版本等,由於歷史原因,這裡的程式碼容易變得比較龐大,啟動耗時難以控制。
所以,滿足業務需要的前提下,didFinishLaunchingWithOptions在主執行緒裡做的事情越少越好。在這一步,我們可以做的優化有:
梳理各個二方/三方庫,找到可以延遲載入的庫,做延遲載入處理,比如放到首頁控制器的viewDidAppear方法裡。
梳理業務邏輯,把可以延遲執行的邏輯,做延遲執行處理。比如檢查新版本、註冊推送通知等邏輯。
避免複雜/多餘的計算。
避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個方法執行完,首頁控制器才能顯示,部分可以延遲建立的檢視應做延遲建立/懶載入處理。
採用效能更好的API。
首頁控制器用純程式碼方式來構建。