1. App的啟動(dòng)分為三個(gè)主要階段:
main()函數(shù)執(zhí)行前
main()函數(shù)執(zhí)行后(從main函數(shù)執(zhí)行,到設(shè)置self.window.rootViewController)
首屏渲染完成后(從設(shè)置self.window.rootViewController到didFinishLaunchWithOptions方法作用域結(jié)束)
main函數(shù)執(zhí)行前,系統(tǒng)會(huì)做的事情:
加載可執(zhí)行文件(App的.o文件集合)
加載動(dòng)態(tài)鏈接庫(kù),進(jìn)行rebase指針調(diào)整和bind符號(hào)綁定
Objc運(yùn)行時(shí)的初始處理,包括Objc相關(guān)類(lèi)注冊(cè)、category注冊(cè)、selector唯一性檢查等
初始化,包括了執(zhí)行+load()方法、attribute((constructor))修飾的函數(shù)的調(diào)用、創(chuàng)建C++靜態(tài)全局變量。
main()函數(shù)執(zhí)行后:
main()函數(shù)執(zhí)行后的階段,指的是從main()函數(shù)執(zhí)行開(kāi)始,到appDelegate的didFinishLaunchingWithOpentions方法里首屏渲染相關(guān)方法執(zhí)行完成。
這里應(yīng)該是從功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是App啟動(dòng)必要的初始化功能,哪些是只需要在對(duì)應(yīng)功能開(kāi)始使用時(shí)才需要初始化的,將這些放到各自合適的階段執(zhí)行。
首屏渲染完成后:
首屏渲染后的這個(gè)階段,指的是didFinishLaunchWithOptions方法作用域內(nèi)執(zhí)行首屏渲染之后的所有方法執(zhí)行完成,即從 設(shè)置了self.window.rootViewController開(kāi)始 到 didFinishLaunchWithOptions方法作用域 結(jié)束。
首屏渲染完成后用戶(hù)就可以看到App的首頁(yè)信息了,把這個(gè)階段內(nèi)卡住主線程的方法解決掉就可以了。
注解:
App啟動(dòng)后,首先加載可執(zhí)行文件,然后加載dyld,然后加載所有依賴(lài)庫(kù),然后調(diào)用所有的+load(),然后調(diào)用main(),然后調(diào)用UIApplicationMain(),然后調(diào)用AppDelegate的代理didFinishLaunchWithOptions.
可執(zhí)行文件是指Mach-O格式的文件,也就是App中所有.o文件的集合體,從這里可以獲取dyld的路徑,然后加載dyld。
dyld是指蘋(píng)果的動(dòng)態(tài)鏈接器,加載dyld后,就會(huì)去初始化運(yùn)行環(huán)境,開(kāi)啟緩存策略,加載依賴(lài)庫(kù),并且會(huì)調(diào)用每一個(gè)依賴(lài)庫(kù)的初始化方法,包括RunTime也是在這里被初始化的,當(dāng)所有的依賴(lài)庫(kù)都被初始化完成后,RunTime會(huì)對(duì)項(xiàng)目中所有的類(lèi)進(jìn)行類(lèi)初始化,調(diào)用所有的+load()方法,最后dyld會(huì)返回main函數(shù)地址,然后main函數(shù)會(huì)被調(diào)用。
知曉上述的流程后,我們就明白為什么優(yōu)化啟動(dòng)速度,要去減少動(dòng)態(tài)庫(kù)加載,要少用+load(),理論明白了之后,我們就要看看具體怎么做了。
動(dòng)態(tài)庫(kù)是指可以共享的代碼文件、資源文件、頭文件等的打包集合體。在Xcode->Targets->General->Link Binary With Libraries可以檢查自己的庫(kù),
-
減少+load()的使用,將里面的內(nèi)容放到渲染結(jié)束后去做,或者用+initialize()代替。+load()方法在main()調(diào)用前就會(huì)調(diào)用,而+initialize()方法是在類(lèi)第一次收到消息后,才會(huì)調(diào)用,兩者的區(qū)別可以參考這里
Main函數(shù)調(diào)用前
Main函數(shù)調(diào)用后
2.具體優(yōu)化方法
(1)減少+load()的使用
使用+initialize()的方法代替+load(),注意把邏輯移動(dòng)到+initialize()時(shí),要注意避免+initialize()的重復(fù)調(diào)用問(wèn)題,可以使用dispatch_once()讓邏輯只執(zhí)行一次。
(2)對(duì)多個(gè)動(dòng)態(tài)庫(kù)進(jìn)行合并
蘋(píng)果公司建議使用更少的動(dòng)態(tài)庫(kù),并且建議在使用動(dòng)態(tài)庫(kù)的數(shù)量較多時(shí),盡量將多個(gè)動(dòng)態(tài)庫(kù)進(jìn)行合并。數(shù)量上,蘋(píng)果公司最多可以支持6個(gè)非系統(tǒng)動(dòng)態(tài)庫(kù)合并為一個(gè)。
(3)優(yōu)化類(lèi)、方法、全局變量
減少加載啟動(dòng)后不會(huì)去使用的類(lèi)或方法;控制C++全局變量的數(shù)量
(4)功能級(jí)別的啟動(dòng)優(yōu)化
main()開(kāi)始執(zhí)行后到首屏渲染完成前,只處理首屏相關(guān)的業(yè)務(wù),其他的都放到首屏渲染完成后去做。
(5)方法級(jí)別的啟動(dòng)優(yōu)化
首先檢查首屏渲染完成前主線程上的耗時(shí)操作,將沒(méi)必要的操作滯后或異步。通常耗時(shí)操作有:加載、編輯、存儲(chǔ)圖片和文件等資源。
3. 查看耗時(shí)
(1)查看Main()調(diào)用前花費(fèi)的總時(shí)間
在Product->Scheme->Edit Scheme->Run->Arguments->Environment Variables->DYLD_PRINT_STATISTICS設(shè)置為YES,就可以在控制臺(tái)中查看main函數(shù)執(zhí)行前總共花費(fèi)的多長(zhǎng)時(shí)間。


(2)查看加載了多少動(dòng)態(tài)庫(kù)
在Product->Scheme->Edit Scheme->Run->Diagnostics->Logging->勾選Dynamic Library Loads,就可以在控制臺(tái)中查看本項(xiàng)目中加載的所有動(dòng)態(tài)庫(kù)(包括系統(tǒng)的和自己的)。

(3)查看Main函數(shù)啟動(dòng)后的耗時(shí)
main函數(shù)調(diào)用后的耗時(shí),可以使用一些工具來(lái)監(jiān)控,有一種非常笨但是很實(shí)用的方法,就是通過(guò)打點(diǎn),在didFinishLaunchingWithOptions開(kāi)始前打一個(gè)點(diǎn),在App顯示完成第一個(gè)界面再打一個(gè)點(diǎn),計(jì)算兩個(gè)點(diǎn)之間的耗時(shí),就可以知道m(xù)ain函數(shù)調(diào)用后到界面顯示出來(lái)的耗時(shí)了,但是這樣只能籠統(tǒng)的知道總的耗時(shí),并不能準(zhǔn)確的知道時(shí)間花在了哪里。
如果想用這個(gè)打點(diǎn)法的話,推薦一個(gè)打點(diǎn)工具BLStopwatch
如果想準(zhǔn)確知道時(shí)間都花在了哪里,推薦使用下面兩種方法。
4. 監(jiān)控App啟動(dòng)耗時(shí),精準(zhǔn)找出時(shí)間都花在了哪里,方便逐一優(yōu)化
準(zhǔn)確監(jiān)控方法有兩種:
定時(shí)抓取主線程上的方法調(diào)用堆棧,計(jì)算一段時(shí)間里各個(gè)方法的耗時(shí)。Xcode自帶的Time Profiler就是用的這種方法。
對(duì)objc_msgSend方法進(jìn)行hook來(lái)掌握所有方法的執(zhí)行耗時(shí)。
根據(jù)這兩種方法,分別實(shí)現(xiàn)兩個(gè)工具,來(lái)監(jiān)控耗時(shí)
由于能力有限,我只根據(jù)第一種方法做出來(lái)一個(gè)計(jì)算某個(gè)線程的耗時(shí)工具,放在了這里BSMonitorTimeTool,大致思路如下:
(1). 通過(guò)定時(shí)器,每隔0.01s,獲取一次主線程的函數(shù)堆棧,將函數(shù)名稱(chēng)、函數(shù)地址、函數(shù)耗時(shí)模型化為TimeModel,保存在callStackDict中,其中key為函數(shù)地址,value為T(mén)imeModel
(2). 定時(shí)執(zhí)行的回調(diào)中,每次都判斷函數(shù)地址是否存在,如果已經(jīng)存在此函數(shù)地址,就講對(duì)應(yīng)的TimeModel中的耗時(shí)增加0.01s;如果不存在此函數(shù)地址,就初始化一個(gè)TimeModel,并將時(shí)間設(shè)置為0.01s。
(3). 當(dāng)主界面顯示完成之后,輸出此callStackDict,即可查看主線程中每個(gè)方法的耗時(shí)
5. 歡迎大家指正錯(cuò)誤,希望能夠共同進(jìn)步
本文章是參考了很多大佬的文章,歡迎各位前去膜拜
- 戴銘大佬的極客時(shí)間02
- 貝聊科技大佬的一次立竿見(jiàn)影的啟動(dòng)時(shí)間優(yōu)化

