iOS啟動時間優(yōu)化方案記錄

image.png

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ù)與代碼,主要包含這三種類型:
    1. __TEXT 包含 Mach header,被執(zhí)行的代碼和只讀常量(如C 字符串)。只讀可執(zhí)行(r-x)。
    2. __DATA 包含全局變量,靜態(tài)變量等。可讀寫(rw-)。
    3. __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 類型包括:

  1. executable:應(yīng)用的二進(jìn)制可執(zhí)行文件;
  2. dylib:動態(tài)鏈接庫;
  3. 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í)行如下操作:

  1. 把可執(zhí)行文件加載到內(nèi)存空間,從可執(zhí)行文件中能夠分析出 dyld 的路徑;
  2. 把 dyld 加載到內(nèi)存;
  3. 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)化方案:

  1. 盡量不使用內(nèi)嵌dylib
  2. 合并已有內(nèi)嵌dylib
  3. 檢查 framework 的 optional 和 required 設(shè)置,如果 framework 在當(dāng)前的 App 支持的 iOS 系統(tǒng)版本中都存在,就設(shè)為 required,因?yàn)樵O(shè)為 optional 會有額外的檢查導(dǎo)致加載變慢;
  4. 使用靜態(tài)庫作為代替;(不過靜態(tài)庫會在編譯期被打進(jìn)可執(zhí)行文件,造成可執(zhí)行文件體積增大,兩者各有利弊,開發(fā)者自行權(quán)衡。)
  5. 懶加載 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)化方案:

  1. 減少 ObjC 類(class)、方法(selector)、分類(category)的數(shù)量,比如合并一些功能,刪除無效的類、方法和分類等(可以借助 AppCode 的 Inspect Code 功能進(jìn)行代碼瘦身);
  2. 減少 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)化方案:

  1. 盡量避免在類的 +load 方法中初始化,可以推遲到 +initiailize 中進(jìn)行;(因?yàn)樵谝粋€ +load 方法中進(jìn)行運(yùn)行時方法替換操作會帶來 4ms 的消耗)

  2. 避免使用 atribute((constructor)) 將方法顯式標(biāo)記為初始化器,而是讓初始化方法調(diào)用時再執(zhí)行。比如用 dispatch_once()、pthread_once() 或 std::once(),相當(dāng)于在第一次使用時才初始化,推遲了一部分工作耗時。:

  3. 減少非基本類型的 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ù)。

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

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