app冷啟動(dòng)三個(gè)階段以及優(yōu)化方案

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ù)叫 fishhookfishhook底層原理直通車。

只靠 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容