1. APP啟動時(shí)間
t(App總啟動時(shí)間) = t1(main()之前的加載時(shí)間) + t2(main()之后的加載時(shí)間)。
t1 = 系統(tǒng)dylib(動態(tài)鏈接庫)和自身App可執(zhí)行文件的加載塞祈; t2 = main方法執(zhí)行之后到AppDelegate類中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
方法執(zhí)行結(jié)束前這段時(shí)間,主要是構(gòu)建第一個界面刽射,并完成渲染展示。
2. 針對于T2階段的優(yōu)化
成本最少剃执,效果明顯。
2.1 didFinishLaunchingWithOptions
一般在這個方法里進(jìn)行初始化操作懈息,并且有些是必須執(zhí)行的肾档,可以適當(dāng)?shù)母鶕?jù)功能的不同適當(dāng)延遲其啟動的時(shí)機(jī):
1.日志、統(tǒng)計(jì)等必須在 APP 一起動就最先配置的事件
2.項(xiàng)目配置辫继、環(huán)境配置怒见、用戶信息的初始化 、推送姑宽、IM等事件
3.其他 SDK 和配置事件
4.可以按需加載的配置遣耍,比如分享
優(yōu)化方案:
第一類:可以仍然放在didFinishLaunchingWithOptions方法里面,
第二類:這個功能要在用戶進(jìn)入APP主體前要加載完炮车,比如放在廣告顯示的時(shí)候
第三類:延遲執(zhí)行部分業(yè)務(wù)邏輯和 UI 配置舵变,可以放在第一個頁面渲染完成之后酣溃,避免首屏加載時(shí)大量的本地/網(wǎng)絡(luò)數(shù)據(jù)讀取
第四類:在使用的時(shí)候可以再去加載
3. 針對于T1階段的優(yōu)化
3.1 測量時(shí)間
通過在工程的scheme
中添加環(huán)境變量DYLD_PRINT_STATISTICS
,設(shè)置值為1纪隙,App啟動加載時(shí)Xcode的控制臺就會有pre-main
各個階段的詳細(xì)耗時(shí)輸出赊豌。但是DYLD_PRINT_STATISTICS
變量打印時(shí)間是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)行時(shí)加載的脚粟,可以被多個 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)庫非常多時(shí)曲聂,相互依賴的符號也更多了,為了節(jié)省解析處理符號的時(shí)間佑惠,OS X 和 iOS 上的動態(tài)鏈接器使用了共享緩存朋腋。OS X 的共享緩存位于 /private/var/db/dyld/,iOS 的則在 /System/Library/Caches/com.apple.dyld/膜楷。
當(dāng)加載一個 Mach-O 文件時(shí)旭咽,dyld 首先會檢查是否存在于共享緩存,存在就直接取出使用赌厅。每一個進(jìn)程都會把這個共享緩存映射到了自己的地址空間中穷绵。這種方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動時(shí)間。
3.2.5 images
images 在這里不是指圖片特愿,而是鏡像仲墨。每個 App 都是以 images 為單位進(jìn)行加載的。images 類型包括:
- executable:應(yīng)用的二進(jìn)制可執(zhí)行文件揍障;
- dylib:動態(tài)鏈接庫目养;
- bundle:資源文件,屬于不能被鏈接的 dylib毒嫡,只能在運(yùn)行時(shí)通過 dlopen() 加載癌蚁。
3.2.6 imageLoader
image表示一個二進(jìn)制文件,里面是被編譯過的符號兜畸、代碼等努释,所以ImageLoader作用是將這些文件加載進(jìn)內(nèi)存,且每一個文件對應(yīng)一個ImageLoader實(shí)例來負(fù)責(zé)加載咬摇。
兩步走: 在程序運(yùn)行時(shí)它先將動態(tài)鏈接的 image 遞歸加載伐蒂, 再從可執(zhí)行文件 image 遞歸加載所有符號。
3.2.7 framework
framework 可以是動態(tài)庫肛鹏,也是靜態(tài)庫饿自,是一個包含 dylib、bundle 和頭文件的文件夾龄坪。
3.3 啟動過程分析與優(yōu)化
啟動一個應(yīng)用時(shí),系統(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,加載它們時(shí)性能開銷較大笋熬。
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 啟動時(shí)是原本是單線程運(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ù)的耗時(shí)也就越少晌畅。所以,優(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)行時(shí)需要做的事情:
- dyld 會注冊所有聲明過的 ObjC 類;
- 將分類插入到類的方法列表中尤蒿;
- 檢查每個 selector 的唯一性郑气。
優(yōu)化方案:
Rebase/Binding 階段優(yōu)化好了,這一步的耗時(shí)也會相應(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)行時(shí)方法替換操作會帶來 4ms 的消耗)
避免使用 atribute((constructor)) 將方法顯式標(biāo)記為初始化器勇婴,而是讓初始化方法調(diào)用時(shí)再執(zhí)行。比如用 dispatch_once()嘱腥、pthread_once() 或 std::once()耕渴,相當(dāng)于在第一次使用時(shí)才初始化,推遲了一部分工作耗時(shí)齿兔。:
減少非基本類型的 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ù)盆偿。