app冷啟動(dòng)概括為3大階段:
1、動(dòng)態(tài)鏈接庫(kù), 啟動(dòng)app時(shí),dyld會(huì)裝載app的可執(zhí)行文件,同時(shí)會(huì)遞歸加載所有依賴的動(dòng)態(tài)庫(kù),進(jìn)行 rebase 指針調(diào)整和 bind 符號(hào)綁定 裝載完畢會(huì)通知Runtime
2、runtime ?1)調(diào)用map—images進(jìn)行可執(zhí)行文件的解析和處理 2)調(diào)用load——methods,調(diào)用所有class和category的load方法 ?3)進(jìn)行objc結(jié)果的初始化 ?4)c++靜態(tài)初始化器和attribute修飾的函數(shù)
3、main() 函數(shù)執(zhí)行后

dylib loading time:
加載動(dòng)態(tài)鏈接庫(kù)所耗的時(shí)間(每個(gè)庫(kù)本身都有依賴關(guān)系,蘋果公司建議使用更少的動(dòng)態(tài)庫(kù),并且建議在使用動(dòng)態(tài)庫(kù)的數(shù)量較多時(shí),盡量將多個(gè)動(dòng)態(tài)庫(kù)進(jìn)行合并。數(shù)量上,蘋果公司建議最多使用 6 個(gè)非系統(tǒng)動(dòng)態(tài)庫(kù)。)
rebase/binding time:
ASLR:全稱為 Address Space Layout Randomization,地址空間布局隨機(jī)化,它將進(jìn)程的某些內(nèi)存空間地址進(jìn)行隨機(jī)化來(lái)增大入侵者預(yù)測(cè)目的地址的難度,從而降低進(jìn)程被成功入侵的風(fēng)險(xiǎn)。
mach-o文件正式采用了ASLR技術(shù),每次啟動(dòng)時(shí)mach-o內(nèi)存地址不一樣。
rebase time:修正偏移的時(shí)間 ?因?yàn)锳SLR技術(shù),每個(gè)函數(shù)的實(shí)際地址是mach-o地址+方法的偏移地址
binding time:當(dāng)引用外部函數(shù)時(shí),比如NSLog,在編譯時(shí)無(wú)法得到其內(nèi)存地址,因?yàn)樗辉诋?dāng)前進(jìn)程中,它存儲(chǔ)在iOS系統(tǒng)的共享緩存空間中(Foundation)。所以調(diào)用外部函數(shù)時(shí),iOS系統(tǒng)在你的可執(zhí)行文件中添加一個(gè)符號(hào),等到運(yùn)行時(shí),由系統(tǒng)去綁定符號(hào),找到真正的外部函數(shù)
objC setup time:?
這一步主要做了以下操作
注冊(cè)O(shè)bjc類 (class registration)
把category的定義插入方法列表 (category registration)
保證每一個(gè)selector唯一 (selctor uniquing)
優(yōu)化方法:
減少class(類),selector(選擇子)以及category(分類)這類元數(shù)據(jù)的數(shù)量(使用AppCode分析未使用的代碼,可以看出有大量?jī)?yōu)化空間)
減少C++虛函數(shù)數(shù)量
使用swift stuct(其實(shí)本質(zhì)上就是為了減少符號(hào)的數(shù)量)
?initializer time:
以上三步屬于靜態(tài)調(diào)整,都是在修改——DATA segment中的內(nèi)容,而這里則開(kāi)始動(dòng)態(tài)調(diào)整,開(kāi)始堆棧中寫(xiě)入內(nèi)容,在這里的工作有以下幾點(diǎn):
用+ initialize方法代替+load方法
使用 dispatch_one() pthread_once() std::once() 代替 C/C++ __ atribute__((constructor))(__ attribute__((constructor))用法解析)
減少靜態(tài)構(gòu)造函數(shù)
不要在初始化方法中調(diào)用 dlopen(),對(duì)性能有影響。因?yàn)?dyld 在 App 開(kāi)始前運(yùn)行,由于此時(shí)是單線程運(yùn)行所以系統(tǒng)會(huì)取消加鎖,但 dlopen() 開(kāi)啟了多線程,系統(tǒng)不得不加鎖,這就嚴(yán)重影響了性能,還可能會(huì)造成死鎖以及產(chǎn)生未知的后果。所以也不要在初始化器中創(chuàng)建線程。
總結(jié)一下
APP的啟動(dòng)由dyld主導(dǎo),將可執(zhí)行文件加載到內(nèi)存,順便加載所有依賴的動(dòng)態(tài)庫(kù)
并由runtime負(fù)責(zé)加載成objc定義的結(jié)構(gòu)
所有初始化工作結(jié)束后,dyld就會(huì)調(diào)用main函數(shù)
接下來(lái)就是UIApplicationMain函數(shù),AppDelegate的application:didFinishLaunchingWithOptions:方法
優(yōu)化:
dyld階段:
1)減少動(dòng)態(tài)庫(kù),合并一些動(dòng)態(tài)庫(kù)(定期清理不必要的動(dòng)態(tài)庫(kù))
2)減少Objc類、分類的數(shù)量、減少Selector數(shù)量(定期清理不必要的類、分類)
3)+load() 方法里的內(nèi)容可以放到首屏渲染完成后再執(zhí)行,或使用 +initialize() 方法替換掉。因?yàn)?,在一個(gè) +load() 方法里,進(jìn)行運(yùn)行時(shí)方法替換操作會(huì)帶來(lái) 4 毫秒的消耗。不要小看這 4 毫秒,積少成多,執(zhí)行 +load() 方法對(duì)啟動(dòng)速度的影響會(huì)越來(lái)越大。
4)控制 C++ 全局變量的數(shù)量。
main() 函數(shù)執(zhí)行后
main() 函數(shù)執(zhí)行后的階段,指的是從 main() 函數(shù)執(zhí)行開(kāi)始,到 appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相關(guān)方法執(zhí)行完成。
首頁(yè)的業(yè)務(wù)代碼都是要在這個(gè)階段,也就是首屏渲染前執(zhí)行的,主要包括了:首屏初始化所需配置文件的讀寫(xiě)操作;首屏列表大數(shù)據(jù)的讀??;首屏渲染的大量計(jì)算等。
功能級(jí)別的啟動(dòng)優(yōu)化
優(yōu)化的思路是: main() 函數(shù)開(kāi)始執(zhí)行后到首屏渲染完成前只處理首屏相關(guān)的業(yè)務(wù),其他非首屏業(yè)務(wù)的初始化、監(jiān)聽(tīng)注冊(cè)、配置文件讀取等都放到首屏渲染完成后去做。
1)在不影響用戶體驗(yàn)的前提下,盡可能將一些操作延遲。不要都放在finishLaunching中
2)按需加載
方法級(jí)別的啟動(dòng)優(yōu)化
檢查首屏渲染完成前主線程上有哪些耗時(shí)方法,將沒(méi)必要的耗時(shí)方法滯后或者異步執(zhí)行。通常情況下,耗時(shí)較長(zhǎng)的方法主要發(fā)生在計(jì)算大量數(shù)據(jù)的情況下,具體的表現(xiàn)就是加載、編輯、存儲(chǔ)圖片和文件等資源。
對(duì) App 啟動(dòng)速度的監(jiān)控,主要有兩種手段:
第一種方法是,定時(shí)抓取主線程上的方法調(diào)用堆棧,計(jì)算一段時(shí)間里各個(gè)方法的耗時(shí)。
Xcode 工具套件里自帶的 Time Profiler ,采用的就是這種方式。這種方式的優(yōu)點(diǎn)是,開(kāi)發(fā)類似工具成本不高,能夠快速開(kāi)發(fā)后集成到你的 App 中,以便在真實(shí)環(huán)境中進(jìn)行檢查。說(shuō)到定時(shí)抓取,就會(huì)涉及到定時(shí)間隔的長(zhǎng)短問(wèn)題。定時(shí)間隔設(shè)置得長(zhǎng)了,會(huì)漏掉一些方法,從而導(dǎo)致檢查出來(lái)的耗時(shí)不精確;而定時(shí)間隔設(shè)置得短了,抓取堆棧這個(gè)方法本身調(diào)用過(guò)多也會(huì)影響整體耗時(shí),導(dǎo)致結(jié)果不準(zhǔn)確。這個(gè)定時(shí)間隔如果小于所有方法執(zhí)行的時(shí)間(比如 0.002 秒),那么基本就能監(jiān)控到所有方法。但這樣做的話,整體的耗時(shí)時(shí)間就不夠準(zhǔn)確。一般將這個(gè)定時(shí)間隔設(shè)置為 0.01 秒。這樣設(shè)置,對(duì)整體耗時(shí)的影響小,不過(guò)很多方法耗時(shí)就不精確了。但因?yàn)檎w耗時(shí)的數(shù)據(jù)更加重要些,單個(gè)方法耗時(shí)精度不高也是可以接受的,所以這個(gè)設(shè)置也是沒(méi)問(wèn)題的。總結(jié)來(lái)說(shuō),定時(shí)抓取主線程調(diào)用棧的方式雖然精準(zhǔn)度不夠高,但也是夠用的。
第二種方法是,對(duì) objc_msgSend 方法進(jìn)行 hook 來(lái)掌握所有方法的執(zhí)行耗時(shí)。hook?objc_msgSend可以查看Facebook 開(kāi)源了一個(gè)庫(kù),這個(gè)庫(kù)叫 fishhook。fishhook底層原理直通車。
只靠 fishhook 就能夠搞定 objc_msgSend 的 hook 了嗎?當(dāng)然還不夠。我前面也說(shuō)了,objc_msgSend 是用匯編語(yǔ)言實(shí)現(xiàn)的,所以我們還需要從匯編層面多加點(diǎn)料。具體耗時(shí)檢測(cè)的完整代碼可查看鏈接,在需要檢測(cè)耗時(shí)時(shí)間的地方調(diào)用 [SMCallTrace start],結(jié)束時(shí)調(diào)用 stop 和 save 就可以打印出方法的調(diào)用層級(jí)和耗時(shí)了。你還可以設(shè)置最大深度和最小耗時(shí)檢測(cè),來(lái)過(guò)濾不需要看到的信息。了這樣一個(gè)檢查方法耗時(shí)的工具,你就可以在每個(gè)版本開(kāi)發(fā)結(jié)束后執(zhí)行一次檢查,統(tǒng)計(jì)總耗時(shí)以及啟動(dòng)階段每個(gè)方法的耗時(shí),有針對(duì)性地觀察啟動(dòng)速度慢的問(wèn)題。如果你在線上做個(gè)灰度開(kāi)關(guān),還可以監(jiān)控線上啟動(dòng)慢的一些特殊情況。
參考:
https://time.geekbang.org/column/article/85331