App啟動速度優(yōu)化看過來
應(yīng)用啟動流程
iOS應(yīng)用的啟動可分為pre-main階段和main()階段,其中系統(tǒng)做的事情依次是:
pre-main階段
- 1.1. 加載應(yīng)用的可執(zhí)行文件
- 1.2. 加載動態(tài)鏈接庫加載器dyld(dynamic loader)
- 1.3. dyld遞歸加載應(yīng)用所有依賴的dylib(dynamic library 動態(tài)鏈接庫)
main()階段
- 2.1. dyld調(diào)用main()
- 2.2. 調(diào)用UIApplicationMain()
- 2.3. 調(diào)用applicationWillFinishLaunching
- 2.4. 調(diào)用didFinishLaunchingWithOptions
啟動耗時(shí)的測量
在進(jìn)行優(yōu)化之前,我們首先應(yīng)該能測量各階段的耗時(shí)。
1. pre-main階段
對于pre-main階段,Apple提供了一種測量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環(huán)境變量DYLD_PRINT_STATISTICS 設(shè)為1 。之后控制臺會輸出類似內(nèi)容:
- 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%)
這樣我們可以清晰的看到每個(gè)耗時(shí)了。
2.main()階段
mian()階段主要是測量mian()函數(shù)開始執(zhí)行到didFinishLaunchingWithOptions執(zhí)行結(jié)束的時(shí)間,我們直接插入代碼就可以了。
- CFAbsoluteTime StartTime;
- int main(int argc, char * argv[]) {
- StartTime = CFAbsoluteTimeGetCurrent();
再在AppDelegate.m文件中用extern聲明全局變量StartTime
- extern CFAbsoluteTime StartTime;
***在didFinishLaunchingWithOptions里,再獲取一下當(dāng)前時(shí)間,與StartTime的差值即是main()階段運(yùn)行耗時(shí)。
- double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);
改善啟動時(shí)間
pre-main階段
在這一階段,我們能做的主要是優(yōu)化dylib
加載 Dylib
之前提到過加載系統(tǒng)的 dylib 很快,因?yàn)橛袃?yōu)化。但加載內(nèi)嵌(embedded)的 dylib 文件很占時(shí)間,所以盡可能把多個(gè)內(nèi)嵌 dylib 合并成一個(gè)來加載,或者使用 static archive。
使用 dlopen() 來在運(yùn)行時(shí)懶加載是不建議的,這么做可能會帶來一些問題,并且總的開銷更大。
Rebase/Binding
之前提過 Rebaing 消耗了大量時(shí)間在 I/O 上,而在之后的 Binding 就不怎么需要 I/O 了,而是將時(shí)間耗費(fèi)在計(jì)算上。所以這兩個(gè)步驟的耗時(shí)是混在一起的。
之前說過可以從查看 __DATA 段中需要修正(fix-up)的指針,所以減少指針數(shù)量才會減少這部分工作的耗時(shí)。對于 ObjC 來說就是減少 Class,selector 和 category 這些元數(shù)據(jù)的數(shù)量。從編碼原則和設(shè)計(jì)模式之類的理論都會鼓勵大家多寫精致短小的類和方法,并將每部分方法獨(dú)立出一個(gè)類別,其實(shí)這會增加啟動時(shí)間。對于 C++ 來說需要減少虛方法,因?yàn)樘摲椒〞?chuàng)建 vtable,這也會在 __DATA 段中創(chuàng)建結(jié)構(gòu)。雖然 C++ 虛方法對啟動耗時(shí)的增加要比 ObjC 元數(shù)據(jù)要少,但依然不可忽視。
Objc setup
大部分ObjC初始化工作已經(jīng)在Rebase/Bind階段做完了,這一步dyld會注冊所有聲明過的ObjC類,將分類插入到類的方法列表里,再檢查每個(gè)selector的唯一性。
在這一步倒沒什么優(yōu)化可做的,Rebase/Bind階段優(yōu)化好了,這一步的耗時(shí)也會減少。
Initializers
到了這一階段,dyld開始運(yùn)行程序的初始化函數(shù),調(diào)用每個(gè)Objc類和分類的+load方法,調(diào)用C/C++ 中的構(gòu)造器函數(shù)(用attribute((constructor))修飾的函數(shù)),和創(chuàng)建非基本類型的C++靜態(tài)全局變量。Initializers階段執(zhí)行完后,dyld開始調(diào)用main()函數(shù)。
在這一步,我們可以做的優(yōu)化有:
- 少在類的+load方法里做事情,盡量把這些事情推遲到+initiailize
- 減少構(gòu)造器函數(shù)個(gè)數(shù),在構(gòu)造器函數(shù)里少做些事情
- 減少C++靜態(tài)全局變量的個(gè)數(shù)
main()階段的優(yōu)化
這一階段的優(yōu)化主要是減少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我們會創(chuàng)建應(yīng)用的window,指定其rootViewController,調(diào)用window的makeKeyAndVisible方法讓其可見。由于業(yè)務(wù)需要,我們會初始化各個(gè)二方/三方庫,設(shè)置系統(tǒng)UI風(fēng)格,檢查是否需要顯示引導(dǎo)頁、是否需要登錄、是否有新版本等,由于歷史原因,這里的代碼容易變得比較龐大,啟動耗時(shí)難以控制。
所以,滿足業(yè)務(wù)需要的前提下,didFinishLaunchingWithOptions在主線程里做的事情越少越好。在這一步,我們可以做的優(yōu)化有:
- 梳理各個(gè)二方/三方庫,找到可以延遲加載的庫,做延遲加載處理,比如放到首頁控制器的viewDidAppear方法里。
- 梳理業(yè)務(wù)邏輯,把可以延遲執(zhí)行的邏輯,做延遲執(zhí)行處理。比如檢查新版本、注冊推送通知等邏輯。
- 避免復(fù)雜/多余的計(jì)算。
- 避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個(gè)方法執(zhí)行完,首頁控制器才能顯示,部分可以延遲創(chuàng)建的視圖應(yīng)做延遲創(chuàng)建/懶加載處理。
- 首頁控制器用純代碼方式來構(gòu)建。
總結(jié)
總結(jié)起來,好像啟動速度優(yōu)化就一句話:讓系統(tǒng)在啟動期間少做一些事。當(dāng)然我們得先清楚工程里做的哪些事是在啟動期間做的、對啟動速度的影響有多大,然后case by case地分析工程代碼,通過放到子線程、延遲加載、懶加載等方式讓系統(tǒng)在啟動期間更輕松些。