技術(shù)調(diào)研
啟動時間計算公式
App總啟動時間 = t1(main()之前的加載時間) + t2(main()之后的加載時間)豌鸡。
t1 = 系統(tǒng)dylib(動態(tài)鏈接庫)和自身App可執(zhí)行文件的加載;
t2 = main方法執(zhí)行之后到AppDelegate類中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執(zhí)行結(jié)束前這段時間,主要是構(gòu)建第一個界面凰慈,并完成渲染展示。
啟動流程
main()調(diào)用之前加載過程
exec() 是一個系統(tǒng)調(diào)用。系統(tǒng)內(nèi)核把應用映射到新的地址空間,且每次起始位置都是隨機的(因為使用 ASLR)逸爵。并將起始位置到 0x000000 這段范圍的進程權(quán)限都標記為不可讀寫不可執(zhí)行。如果是 32 位進程凹嘲,這個范圍至少是 4KB师倔;對于 64 位進程則至少是 4GB。NULL 指針引用和指針截斷誤差都是會被它捕獲周蹭。
dylib loading
從主執(zhí)行文件的 header 獲取到需要加載的所依賴動態(tài)庫列表趋艘,而 header 早就被內(nèi)核映射過。然后它需要找到每個 dylib凶朗,然后打開文件讀取文件起始位置瓷胧,確保它是 Mach-O 文件。接著會找到代碼簽名并將其注冊到內(nèi)核棚愤。然后在 dylib 文件的每個 segment 上調(diào)用 mmap()搓萧。應用所依賴的 dylib 文件可能會再依賴其他 dylib,所以 dyld 所需要加載的是動態(tài)庫列表一個遞歸依賴的集合宛畦。一般應用會加載 100 到 400 個 dylib 文件瘸洛,但大部分都是系統(tǒng) dylib,它們會被預先計算和緩存起來次和,加載速度很快货矮。
rebase/bind
由于ASLR(address space layout randomization)的存在,可執(zhí)行文件和動態(tài)鏈接庫在虛擬內(nèi)存中的加載地址每次啟動都不固定斯够,所以需要這2步來修復鏡像中的資源指針,來指向正確的地址喧锦。 rebase修復的是指向當前鏡像內(nèi)部的資源指針读规; 而bind指向的是鏡像外部的資源指針。
rebase步驟先進行燃少,需要把鏡像讀入內(nèi)存束亏,并以page為單位進行加密驗證,保證不會被篡改阵具,所以這一步的瓶頸在IO碍遍。bind在其后進行,由于要查詢符號表阳液,來指向跨鏡像的資源怕敬,加上在rebase階段,鏡像已被讀入和加密驗證帘皿,所以這一步的瓶頸在于CPU計算东跪。
通過命令行可以查看相關(guān)的資源指針:
xcrun dyldinfo -rebase -bind -lazy_bind myApp.App/myApp
優(yōu)化該階段的關(guān)鍵在于減少__DATA segment中的指針數(shù)量。我們可以優(yōu)化的點有:
減少Objc類數(shù)量, 減少selector數(shù)量
減少C++虛函數(shù)數(shù)量
轉(zhuǎn)而使用swift struct(其實本質(zhì)上就是為了減少符號的數(shù)量)
Objc Runtime
這一步主要工作是:
注冊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ù),使用 +initialize 來替代 +load
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ù)中有繁重的工作第献,那么會拖慢啟動速度
Objc的load函數(shù)和C++的靜態(tài)構(gòu)造函數(shù)采用由底向上的方式執(zhí)行,來保證每個執(zhí)行的方法熬的,都可以找到所依賴的動態(tài)庫痊硕。
main()調(diào)用之后的加載時間
在main()被調(diào)用之后,App的主要工作就是初始化必要的服務(wù)押框,顯示首頁內(nèi)容等岔绸。而我們的優(yōu)化也是圍繞如何能夠快速展現(xiàn)首頁來開展。 App通常在AppDelegate類中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中創(chuàng)建首頁需要展示的view橡伞,然后在當前runloop的末尾盒揉,主動調(diào)用CA::Transaction::commit完成視圖的渲染。
而視圖的渲染主要涉及三個階段:
準備階段 這里主要是圖片的解碼
布局階段 首頁所有UIView的- (void)layoutSubViews()運行
繪制階段 首頁所有UIView的- (void)drawRect:(CGRect)rect運行
再加上啟動之后必要服務(wù)的啟動兑徘、必要數(shù)據(jù)的創(chuàng)建和讀取刚盈,這些就是我們可以嘗試優(yōu)化的地方
因此,main()函數(shù)調(diào)用之前我們可以優(yōu)化的點有:
不使用xib挂脑,直接視用代碼加載首頁視圖藕漱。
NSUserDefaults實際上是在Library文件夾下會生產(chǎn)一個plist文件,如果文件太大的話一次能讀取到內(nèi)存中可能很耗時崭闲,這個影響需要評估肋联,如果耗時很大的話需要拆分(需考慮老版本覆蓋安裝兼容問題)。
每次用NSLog方式打印會隱式的創(chuàng)建一個Calendar, 僅僅針對內(nèi)測版輸出log刁俭。
梳理應用啟動時發(fā)送的所有網(wǎng)絡(luò)請求橄仍,統(tǒng)一在異步線程請求。
并行初始化各個業(yè)務(wù)牍戚。
優(yōu)化方案
main()調(diào)用之前加載過程,優(yōu)化內(nèi)容
減少framework引用
刪除無用類侮繁,無用函數(shù)
減少+load 函數(shù)使用
main()調(diào)用之后, 優(yōu)化內(nèi)容
將業(yè)務(wù)相關(guān)的啟動放入group async 內(nèi)進行,并發(fā)期待如孝。
最后在wait 全部執(zhí)行后宪哩,進行splash 頁面。
業(yè)務(wù)不相關(guān)的放入全局global_queue 內(nèi)進行初始化
// 自定義queue
dispatch_group_t lanchGroup = dispatch_group_create();
dispatch_queue_t lanchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 全局并行queue
dispatch_group_async(lanchGroup, lanchQueue, ^{});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{});
參考鏈接:https://github.com/hongruqi/WTAppLauncher
參考鏈接https://chars.tech/blog/ios-app-launch-time-optimize/