App啟動速度優化看過來
應用啟動流程
iOS應用的啟動可分為pre-main階段和main()階段,其中系統做的事情依次是:
pre-main階段
- 1.1. 加載應用的可執行文件
- 1.2. 加載動態鏈接庫加載器dyld(dynamic loader)
- 1.3. dyld遞歸加載應用所有依賴的dylib(dynamic library 動態鏈接庫)
main()階段
- 2.1. dyld調用main()
- 2.2. 調用UIApplicationMain()
- 2.3. 調用applicationWillFinishLaunching
- 2.4. 調用didFinishLaunchingWithOptions
啟動耗時的測量
在進行優化之前,我們首先應該能測量各階段的耗時。
1. pre-main階段
對于pre-main階段,Apple提供了一種測量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環境變量DYLD_PRINT_STATISTICS 設為1 。之后控制臺會輸出類似內容:
- Total pre-main time: 228.41 milliseconds (100.0%)
- dylib loading time: 82.35 milliseconds (36.0%)
- rebase/binding time: 6.12 milliseconds (2.6%)
- ObjC setup time: 7.82 milliseconds (3.4%)
- initializer time: 132.02 milliseconds (57.8%)
- slowest intializers :
- libSystem.B.dylib : 122.07 milliseconds (53.4%)
- CoreFoundation : 5.59 milliseconds (2.4%)
這樣我們可以清晰的看到每個耗時了。
2.main()階段
mian()階段主要是測量mian()函數開始執行到didFinishLaunchingWithOptions執行結束的時間,我們直接插入代碼就可以了。
- 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階段
在這一階段,我們能做的主要是優化dylib
加載 Dylib
之前提到過加載系統的 dylib 很快,因為有優化。但加載內嵌(embedded)的 dylib 文件很占時間,所以盡可能把多個內嵌 dylib 合并成一個來加載,或者使用 static archive。
使用 dlopen() 來在運行時懶加載是不建議的,這么做可能會帶來一些問題,并且總的開銷更大。
Rebase/Binding
之前提過 Rebaing 消耗了大量時間在 I/O 上,而在之后的 Binding 就不怎么需要 I/O 了,而是將時間耗費在計算上。所以這兩個步驟的耗時是混在一起的。
之前說過可以從查看 __DATA 段中需要修正(fix-up)的指針,所以減少指針數量才會減少這部分工作的耗時。對于 ObjC 來說就是減少 Class,selector 和 category 這些元數據的數量。從編碼原則和設計模式之類的理論都會鼓勵大家多寫精致短小的類和方法,并將每部分方法獨立出一個類別,其實這會增加啟動時間。對于 C++ 來說需要減少虛方法,因為虛方法會創建 vtable,這也會在 __DATA 段中創建結構。雖然 C++ 虛方法對啟動耗時的增加要比 ObjC 元數據要少,但依然不可忽視。
Objc setup
大部分ObjC初始化工作已經在Rebase/Bind階段做完了,這一步dyld會注冊所有聲明過的ObjC類,將分類插入到類的方法列表里,再檢查每個selector的唯一性。
在這一步倒沒什么優化可做的,Rebase/Bind階段優化好了,這一步的耗時也會減少。
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個方法執行完,首頁控制器才能顯示,部分可以延遲創建的視圖應做延遲創建/懶加載處理。
- 首頁控制器用純代碼方式來構建。
總結
總結起來,好像啟動速度優化就一句話:讓系統在啟動期間少做一些事。當然我們得先清楚工程里做的哪些事是在啟動期間做的、對啟動速度的影響有多大,然后case by case地分析工程代碼,通過放到子線程、延遲加載、懶加載等方式讓系統在啟動期間更輕松些。