
1. APP啟動時間
t(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)建第一個界面,并完成渲染展示。
2. 針對于T2階段的優(yōu)化
成本最少,效果明顯。
2.1 didFinishLaunchingWithOptions
一般在這個方法里進(jìn)行初始化操作,并且有些是必須執(zhí)行的,可以適當(dāng)?shù)母鶕?jù)功能的不同適當(dāng)延遲其啟動的時機(jī):
1.日志、統(tǒng)計(jì)等必須在 APP 一起動就最先配置的事件
2.項(xiàng)目配置、環(huán)境配置、用戶信息的初始化 、推送、IM等事件
3.其他 SDK 和配置事件
4.可以按需加載的配置,比如分享
優(yōu)化方案:
第一類:可以仍然放在didFinishLaunchingWithOptions方法里面,
第二類:這個功能要在用戶進(jìn)入APP主體前要加載完,比如放在廣告顯示的時候
第三類:延遲執(zhí)行部分業(yè)務(wù)邏輯和 UI 配置,可以放在第一個頁面渲染完成之后,避免首屏加載時大量的本地/網(wǎng)絡(luò)數(shù)據(jù)讀取
第四類:在使用的時候可以再去加載
3. 針對于T1階段的優(yōu)化
3.1 測量時間
通過在工程的scheme中添加環(huán)境變量DYLD_PRINT_STATISTICS,設(shè)置值為1,App啟動加載時Xcode的控制臺就會有pre-main各個階段的詳細(xì)耗時輸出。但是DYLD_PRINT_STATISTICS變量打印時間是iOS10以后才支持的功能,所以需要用iOS10系統(tǒng)及以上的機(jī)器來做測試。
Total pre-main time: 1.1 seconds (100.0%)
dylib loading time: 458.55 milliseconds (38.8%)
rebase/binding time: 145.48 milliseconds (12.3%)
ObjC setup time: 28.99 milliseconds (2.4%)
initializer time: 548.53 milliseconds (46.4%)
slowest intializers :
libSystem.B.dylib : 5.85 milliseconds (0.4%)
libglInterpose.dylib : 376.73 milliseconds (31.8%)
AFNetworking : 54.63 milliseconds (4.6%)
WKWebKit : 48.15 milliseconds (4.0%)
如果想查看更詳細(xì)的信息,就設(shè)置DYLD_PRINT_STATISTICS_DETAILS為1:
total time: 2.4 seconds (100.0%)
total images loaded: 459 (424 from dyld shared cache)
total segments mapped: 114, into 10022 pages
total images loading time: 1.7 seconds (71.8%)
total load time in ObjC: 12.78 milliseconds (0.5%)
total debugger pause time: 1.6 seconds (69.3%)
total dtrace DOF registration time: 0.00 milliseconds (0.0%)
total rebase fixups: 149,991
total rebase fixups time: 12.35 milliseconds (0.5%)
total binding fixups: 72,264
total binding fixups time: 38.72 milliseconds (1.6%)
total weak binding fixups time: 36.61 milliseconds (1.5%)
total redo shared cached bindings time: 27.73 milliseconds (1.1%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 574.31 milliseconds (23.9%)
libSystem.B.dylib : 7.44 milliseconds (0.3%)
libBacktraceRecording.dylib : 6.06 milliseconds (0.2%)
libMainThreadChecker.dylib : 16.50 milliseconds (0.6%)
libglInterpose.dylib : 398.79 milliseconds (16.6%)
libMTLCapture.dylib : 14.42 milliseconds (0.6%)
AFNetworking : 55.91 milliseconds (2.3%)
ZWWKWebKit : 52.46 milliseconds (2.1%)
Demo : 18.29 milliseconds (0.7%)
total symbol trie searches: 283620
total symbol table binary searches: 0
total images defining weak symbols: 51
total images using weak symbols: 119
3.2 理論理解
3.2.1 Mach-O文件
Mach-O(Mach Object File Format)是一種用于記錄可執(zhí)行文件、對象代碼、共享庫、動態(tài)加載代碼和內(nèi)存轉(zhuǎn)儲的文件格式。App 編譯生成的二進(jìn)制可執(zhí)行文件就是 Mach-O 格式的,iOS 工程所有的類編譯后會生成對應(yīng)的目標(biāo)文件 .o 文件,而這個可執(zhí)行文件就是這些 .o 文件的集合。
Mach-O 文件主要由三部分組成:
- Mach header:描述 Mach-O 的 CPU 架構(gòu)、文件類型以及加載命令等;
- Load commands:描述了文件中數(shù)據(jù)的具體組織結(jié)構(gòu),不同的數(shù)據(jù)類型使用不同的加載命令;
- Data:Data 中的每個段(segment)的數(shù)據(jù)都保存在這里,每個段都有一個或多個 Section,它們存放了具體的數(shù)據(jù)與代碼,主要包含這三種類型:
- __TEXT 包含 Mach header,被執(zhí)行的代碼和只讀常量(如C 字符串)。只讀可執(zhí)行(r-x)。
- __DATA 包含全局變量,靜態(tài)變量等。可讀寫(rw-)。
- __LINKEDIT 包含了加載程序的元數(shù)據(jù),比如函數(shù)的名稱和地址。只讀(r–-)。
3.2.2 dylib
dylib也是一種 Mach-O 格式的文件,后綴名為 .dylib 的文件就是動態(tài)庫(也叫動態(tài)鏈接庫)。動態(tài)庫是運(yùn)行時加載的,可以被多個 App 的進(jìn)程共用。
如果想知道 TestDemo 中依賴的所有動態(tài)庫,可以通過下面的指令實(shí)現(xiàn):
otool -L /TestDemo.app/TestDemo
3.2.3 dyld
動態(tài)鏈接器,其本質(zhì)也是 Mach-O 文件,一個專門用來加載 dylib 文件的庫。
dyld 位于 /usr/lib/dyld,可以在 mac 和越獄機(jī)中找到。dyld 會將 App 依賴的動態(tài)庫和 App 文件加載到
內(nèi)存后執(zhí)行。
3.2.4 dyld shared cache
是動態(tài)庫共享緩存,當(dāng)需要加載的動態(tài)庫非常多時,相互依賴的符號也更多了,為了節(jié)省解析處理符號的時間,OS X 和 iOS 上的動態(tài)鏈接器使用了共享緩存。OS X 的共享緩存位于 /private/var/db/dyld/,iOS 的則在 /System/Library/Caches/com.apple.dyld/。
當(dāng)加載一個 Mach-O 文件時,dyld 首先會檢查是否存在于共享緩存,存在就直接取出使用。每一個進(jìn)程都會把這個共享緩存映射到了自己的地址空間中。這種方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動時間。
3.2.5 images
images 在這里不是指圖片,而是鏡像。每個 App 都是以 images 為單位進(jìn)行加載的。images 類型包括:
- executable:應(yīng)用的二進(jìn)制可執(zhí)行文件;
- dylib:動態(tài)鏈接庫;
- bundle:資源文件,屬于不能被鏈接的 dylib,只能在運(yùn)行時通過 dlopen() 加載。
3.2.6 imageLoader
image表示一個二進(jìn)制文件,里面是被編譯過的符號、代碼等,所以ImageLoader作用是將這些文件加載進(jìn)內(nèi)存,且每一個文件對應(yīng)一個ImageLoader實(shí)例來負(fù)責(zé)加載。
兩步走: 在程序運(yùn)行時它先將動態(tài)鏈接的 image 遞歸加載, 再從可執(zhí)行文件 image 遞歸加載所有符號。
3.2.7 framework
framework 可以是動態(tài)庫,也是靜態(tài)庫,是一個包含 dylib、bundle 和頭文件的文件夾。
3.3 啟動過程分析與優(yōu)化
啟動一個應(yīng)用時,系統(tǒng)會通過fork()方法來新創(chuàng)建一個進(jìn)程,然后執(zhí)行鏡像通過exec()來替換為另一個可執(zhí)行程序,然后執(zhí)行如下操作:
- 把可執(zhí)行文件加載到內(nèi)存空間,從可執(zhí)行文件中能夠分析出 dyld 的路徑;
- 把 dyld 加載到內(nèi)存;
- dyld 從可執(zhí)行文件的依賴開始,遞歸加載所有的依賴動態(tài)鏈接庫 dylib 并進(jìn)行相應(yīng)的初始化操作。
結(jié)合上面 pre-main 打印的結(jié)果,我們可以大致了解整個啟動過程如下圖所示:
exec() -> Load Executable -> Load Dyld -> Load Dylibs -> Rebase -> Binding ->ObjCSetUp -> Initializers
3.3.1 Load Dylibs
這一步,指的是動態(tài)庫加載。在此階段,dyld 會:
- 分析 App 依賴的所有 dylib;
- 找到 dylib 對應(yīng)的 Mach-O 文件;
- 打開、讀取這些 Mach-O 文件,并驗(yàn)證其有效性;
- 在系統(tǒng)內(nèi)核中注冊代碼簽名;
- 對 dylib 的每一個 segment 調(diào)用 mmap()。
一般情況下,iOS App 需要加載 100-400 個 dylibs。這些動態(tài)庫包括系統(tǒng)的,也包括開發(fā)者手動引入的。其中大部分 dylib 都是系統(tǒng)庫,系統(tǒng)已經(jīng)做了優(yōu)化,因此開發(fā)者更應(yīng)關(guān)心自己手動集成的內(nèi)嵌 dylib,加載它們時性能開銷較大。
App 中依賴的 dylib 越少越好,Apple 官方建議盡量將內(nèi)嵌 dylib 的個數(shù)維持在6個以內(nèi)。
優(yōu)化方案:
- 盡量不使用內(nèi)嵌dylib
- 合并已有內(nèi)嵌dylib
- 檢查 framework 的 optional 和 required 設(shè)置,如果 framework 在當(dāng)前的 App 支持的 iOS 系統(tǒng)版本中都存在,就設(shè)為 required,因?yàn)樵O(shè)為 optional 會有額外的檢查導(dǎo)致加載變慢;
- 使用靜態(tài)庫作為代替;(不過靜態(tài)庫會在編譯期被打進(jìn)可執(zhí)行文件,造成可執(zhí)行文件體積增大,兩者各有利弊,開發(fā)者自行權(quán)衡。)
- 懶加載 dylib。(但使用 dlopen() 對性能會產(chǎn)生影響,因?yàn)?App 啟動時是原本是單線程運(yùn)行,系統(tǒng)會取消加鎖,但 dlopen() 開啟了多線程,系統(tǒng)不得不加鎖,這樣不僅會使性能降低,可能還會造成死鎖及未知的后果,不是很推薦這種做法。)
3.3.2 Rebase/Binding
指針重定位。
在 dylib 的加載過程中,系統(tǒng)為了安全考慮,引入了 ASLR(Address Space Layout Randomization)技術(shù)和代碼簽名。由于 ASLR 的存在,鏡像會在新的隨機(jī)地址(actual_address)上加載,和之前指針指向的地址(preferred_address)會有一個偏差(slide,slide=actual_address-preferred_address),因此 dyld 需要修正這個偏差,指向正確的地址。具體通過這兩步實(shí)現(xiàn):
第一步:Rebase,在 image 內(nèi)部調(diào)整指針的指向。將 image 讀入內(nèi)存,并以 page 為單位進(jìn)行加密驗(yàn)證,保證不會被篡改,性能消耗主要在 IO。
第二步:Binding,符號綁定。將指針指向 image 外部的內(nèi)容。查詢符號表,設(shè)置指向鏡像外部的指針,性能消耗主要在 CPU 計(jì)算。
通過以下命令可以查看 rebase 和 bind 等信息:
xcrun dyldinfo -rebase -bind -lazy_bind TestDemo.app/TestDemo
通過 LC_DYLD_INFO_ONLY 可以查看各種信息的偏移量和大小。如果想要更方便直觀地查看,推薦使用 MachOView 工具。
指針數(shù)量越少,指針修復(fù)的耗時也就越少。所以,優(yōu)化該階段的關(guān)鍵就是減少 __DATA 段中的指針數(shù)量。
優(yōu)化方案:
- 減少 ObjC 類(class)、方法(selector)、分類(category)的數(shù)量,比如合并一些功能,刪除無效的類、方法和分類等(可以借助 AppCode 的 Inspect Code 功能進(jìn)行代碼瘦身);
- 減少 C++ 虛函數(shù);(虛函數(shù)會創(chuàng)建 vtable,這也會在 __DATA 段中創(chuàng)建結(jié)構(gòu)。)
3.3.3 ObjC Setup
完成 Rebase 和 Bind 之后,通知 runtime 去做一些代碼運(yùn)行時需要做的事情:
- dyld 會注冊所有聲明過的 ObjC 類;
- 將分類插入到類的方法列表中;
- 檢查每個 selector 的唯一性。
優(yōu)化方案:
Rebase/Binding 階段優(yōu)化好了,這一步的耗時也會相應(yīng)減少。
3.3.4 Initializers
Rebase 和 Binding 屬于靜態(tài)調(diào)整(fix-up),修改的是 __DATA 段中的內(nèi)容,而這里則開始動態(tài)調(diào)整,往堆和棧中寫入內(nèi)容。具體工作有:
- 調(diào)用每個 Objc 類和分類中的 +load 方法;
- 調(diào)用 C/C++ 中的構(gòu)造器函數(shù)(用 attribute((constructor)) 修飾的函數(shù));
- 創(chuàng)建非基本類型的 C++ 靜態(tài)全局變量。
優(yōu)化方案:
盡量避免在類的 +load 方法中初始化,可以推遲到 +initiailize 中進(jìn)行;(因?yàn)樵谝粋€ +load 方法中進(jìn)行運(yùn)行時方法替換操作會帶來 4ms 的消耗)
避免使用 atribute((constructor)) 將方法顯式標(biāo)記為初始化器,而是讓初始化方法調(diào)用時再執(zhí)行。比如用 dispatch_once()、pthread_once() 或 std::once(),相當(dāng)于在第一次使用時才初始化,推遲了一部分工作耗時。:
減少非基本類型的 C++ 靜態(tài)全局變量的個數(shù)。(因?yàn)檫@類全局變量通常是類或者結(jié)構(gòu)體,如果在構(gòu)造函數(shù)中有繁重的工作,就會拖慢啟動速度)
3.3.5 總結(jié)pre-main 階段可行的優(yōu)化方案
重新梳理架構(gòu),減少不必要的內(nèi)置動態(tài)庫數(shù)量;
進(jìn)行代碼瘦身,合并或刪除無效的ObjC類、Category、方法、C++ 靜態(tài)全局變量等;
將不必須在 +load 方法中執(zhí)行的任務(wù)延遲到 +initialize 中;
減少 C++ 虛函數(shù)。