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

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