冷啟動和熱啟動
1.冷啟動:APP 的第一次打開,或者被kill掉之后重新打開的啟動過程為冷啟動牧愁。
2.熱啟動:就是按下home鍵的時候了嚎,app還存在一段時間,這時點擊app馬上就能恢復(fù)到原狀態(tài)咧七,這種啟動我們稱為熱啟動衰齐。
應(yīng)用啟動時間,直接影響用戶對一款應(yīng)用的判斷和使用體驗继阻,冷啟動的時間的是我們關(guān)注比較多的方向耻涛,也是需要注重優(yōu)化的地方,解析會詳細(xì)介紹app冷啟動的過程和優(yōu)化策略瘟檩。
冷啟動的耗時計算
t(App總啟動時間) = t1(main()之前的加載時間) + t2(main()之后的加載時間)抹缕。
main()之前的加載過程
大致的過程如下:
- 加載dyld到App進(jìn)程
- 加載動態(tài)庫(包括所依賴的所有動態(tài)庫)
- Rebase image
- Bind image
- Objc setup
- 初始化
加載動態(tài)庫
App開始啟動后,系統(tǒng)首先加載可執(zhí)行文件(自身App的所有.o文件的集合)墨辛,然后使用dyld
加載動態(tài)鏈接庫卓研。dyld是一個專門用來加載動態(tài)鏈接庫的庫。
dyld的全稱是dynamic loader,它的作用是加載一個進(jìn)程所需要的image(可執(zhí)行文件奏赘、動態(tài)鏈接庫等)寥闪,dyld是開源的。
執(zhí)行從dyld開始磨淌,dyld從可執(zhí)行文件的依賴開始, 遞歸加載所有的依賴動態(tài)鏈接庫疲憋。在每個動態(tài)庫的加載過程中, dyld需要:
- 分析所依賴的動態(tài)庫
- 找到動態(tài)庫的mach-o文件
- 打開文件
- 驗證文件
- 在系統(tǒng)核心注冊文件簽名
- 對動態(tài)庫的每一個segment調(diào)用mmap()
動態(tài)鏈接庫包括:iOS 中用到的所有系統(tǒng) framework梁只,加載
OC runtime
方法的libobjc
缚柳,系統(tǒng)級別的libSystem
,例如libdispatch
(GCD)和libsystem_blocks
(Block)搪锣。動態(tài)鏈接庫有以下好處:代碼共用秋忙,很多程序都動態(tài)鏈接了這些 lib,但它們在內(nèi)存和磁盤中只有一份构舟; 易于維護(hù)灰追,由于被依賴的 lib 是程序執(zhí)行時才鏈接的,所以這些 lib 很容易做更新旁壮。
通常的监嗜,一個App需要加載100到400個dylibs, 但是其中的系統(tǒng)庫被優(yōu)化抡谐,可以很快的加載裁奇。 針對這一步驟的優(yōu)化有:
- 減少非系統(tǒng)庫的依賴
- 合并非系統(tǒng)庫
- 使用靜態(tài)資源,比如把代碼加入主程序
Rebase && Bind
ASLR
(Address space layout randomization)地址空間布局隨機化麦撵,是一種針對緩沖區(qū)溢出的安全保護(hù)技術(shù)刽肠,通過對堆、棧免胃、共享庫映射等線性區(qū)布局的隨機化音五,增加攻擊者預(yù)測目的地址的難度,防止攻擊者直接定位攻擊代碼位置羔沙,達(dá)到阻止溢出攻擊的目的躺涝。大部分主流的操作系統(tǒng)已經(jīng)實現(xiàn)了ASLR
,Apple在iOS4.3開始導(dǎo)入了ASLR
扼雏。
由于ASLR
的存在坚嗜,可執(zhí)行文件和動態(tài)鏈接庫在虛擬內(nèi)存中的加載地址每次啟動都不固定,所以需要這2步來修復(fù)鏡像中的資源指針诗充,來指向正確的地址苍蔬。 Rebase
修復(fù)的是指向當(dāng)前鏡像內(nèi)部的資源指針; 而Bind
指向的是鏡像外部的資源指針蝴蜓。
- Rebase步驟先進(jìn)行碟绑,需要把鏡像讀入內(nèi)存俺猿,并以page為單位進(jìn)行加密驗證,保證不會被篡改格仲,所以這一步的瓶頸在IO押袍。
- Bind在其后進(jìn)行,由于要查詢符號表抓狭,來指向跨鏡像的資源伯病,加上在rebase階段,鏡像已被讀入和加密驗證否过,所以這一步的瓶頸在于CPU計算。
優(yōu)化該階段的關(guān)鍵在于減少__DATA segment
中的指針數(shù)量惭蟋。我們可以優(yōu)化的點有:
- 減少Objc類數(shù)量苗桂, 減少selector數(shù)量
- 減少C++虛函數(shù)數(shù)量
- 轉(zhuǎn)而使用swift struct(其實本質(zhì)上就是為了減少符號的數(shù)量)
Objc setup
這一步主要工作是:
- 注冊O(shè)bjc類 (class registration)
- 把category的定義插入方法列表 (category registration)
- 保證每一個selector唯一 (selctor uniquing)
由于之前2步驟的優(yōu)化,這一步實際上沒有什么可做的告组。
Initializers
以上三步屬于靜態(tài)調(diào)整(fix-up)煤伟,都是在修改__DATA segment
中的內(nèi)容,而這里則開始動態(tài)調(diào)整木缝,開始在堆和堆棧中寫入內(nèi)容便锨。 在這里的工作有:
- Objc的+load()函數(shù)
- C++的構(gòu)造函數(shù)屬性函數(shù) 形如attribute((constructor)) void DoSomeInitializationWork()
- 非基本類型的C++靜態(tài)全局變量的創(chuàng)建(通常是類或結(jié)構(gòu)體)(non-trivial initializer) 比如一個全局靜態(tài)結(jié)構(gòu)體的構(gòu)建,如果在構(gòu)造函數(shù)中有繁重的工作我碟,那么會拖慢啟動速度放案。
至此,可執(zhí)行文件中和動態(tài)庫所有的符號(Class矫俺,Protocol吱殉,Selector,IMP厘托,…)都已經(jīng)按格式成功加載到內(nèi)存中友雳,被 runtime 所管理,再這之后铅匹,runtime 的那些方法(動態(tài)添加 Class押赊、swizzle 等等)才能生效。
main()之前的加載時間
在不越獄的情況下包斑,以往很難精確的測量在main()函數(shù)之前的啟動耗時流礁,因而我們也往往容易忽略掉這部分?jǐn)?shù)據(jù)。小型App確實不需要太過關(guān)注這部分舰始。但如果是大型App(自定義的動態(tài)庫超過50個崇棠、或編譯結(jié)果二進(jìn)制文件超過30MB),這部分耗時將會變得突出丸卷。所幸枕稀,蘋果已經(jīng)在Xcode中加入這部分的支持。
在Xcode的菜單中選擇Project
→Scheme
→Edit Scheme
,然后找到 Run
→ Environment Variables
→+
萎坷,添加name為DYLD_PRINT_STATISTICS
凹联,value為1的環(huán)境變量。
Total pre-main time: 43.00 milliseconds (100.0%)
dylib loading time: 19.01 milliseconds (44.2%)
rebase/binding time: 1.77 milliseconds (4.1%)
ObjC setup time: 3.98 milliseconds (9.2%)
initializer time: 18.17 milliseconds (42.2%)
slowest intializers :
libSystem.B.dylib : 2.56 milliseconds (5.9%)
libBacktraceRecording.dylib : 3.00 milliseconds (6.9%)
libMainThreadChecker.dylib : 8.26 milliseconds (19.2%)
ModelIO : 1.37 milliseconds (3.1%)
main()前的優(yōu)化總結(jié):
- 減少不必要的framework哆档,因為動態(tài)鏈接比較耗時
- check framework應(yīng)當(dāng)設(shè)為optional和required蔽挠,如果該framework在當(dāng)前App支持的所有iOS系統(tǒng)版本都存在,那么就設(shè)為required瓜浸,否則就設(shè)為optional澳淑,因為optional會有些額外的檢查
- 合并或者刪減一些OC類,關(guān)于清理項目中沒用到的類插佛,使用工具AppCode代碼檢查功能杠巡,查到當(dāng)前項目中沒有用到的類、方法雇寇、變量等:
1. 刪減一些無用的靜態(tài)變量
2. 刪減沒有被調(diào)用到或者已經(jīng)廢棄的方法氢拥,方法見:StackOverflow、Apple - 將不必須在+load方法中做的事情延遲到+initialize中
- 盡量不要用C++虛函數(shù)(創(chuàng)建虛函數(shù)表有開銷)
main()調(diào)用之后優(yōu)化
從main()函數(shù)開始至applicationWillFinishLaunching結(jié)束锨侯,我們統(tǒng)一稱為main()函數(shù)之后的部分嫩海,在main()被調(diào)用之后,App的主要工作就是初始化必要的服務(wù)囚痴,顯示首頁內(nèi)容等叁怪。而我們的優(yōu)化也是圍繞如何能夠快速展現(xiàn)首頁來開展。 main()函數(shù)之后耗時的影響因素:
- 執(zhí)行main()函數(shù)的耗時
- 執(zhí)行applicationWillFinishLaunching的耗時
- rootViewController及其childViewController的加載渡讼、view及其subviews的加載
這個過程的時間統(tǒng)計可以在起止位置埋點獲取時間戳骂束,開始位置時main()開始執(zhí)行的位置,結(jié)束位置是applicationWillFinishLaunching開始執(zhí)行的位置成箫,將兩個時間戳相減可以獲取main()調(diào)用之后的耗時展箱。對于main()函數(shù)調(diào)用之后我們可以優(yōu)化的點有:
- 不使用xib,直接使用代碼加載首頁視圖蹬昌;
- NSUserDefaults實際上是在Library文件夾下會生產(chǎn)一個plist文件混驰,如果文件太大的話一次能讀取到內(nèi)存中可能很耗時,這個影響需要評估皂贩,如果耗時很大的話需要拆分(需考慮老版本覆蓋安裝兼容問題)栖榨;
- 每次用NSLog方式打印會隱式的創(chuàng)建一個Calendar,因此需要刪減啟動時各業(yè)務(wù)方打的log明刷,或者僅僅針對內(nèi)測版輸出log婴栽;
- 梳理應(yīng)用啟動時發(fā)送的所有網(wǎng)絡(luò)請求,是否可以統(tǒng)一在異步線程請求辈末;
以上就是iOS應(yīng)用啟動時間統(tǒng)計和優(yōu)化方案總結(jié)
參考文獻(xiàn):