iOS 啟動(dòng)優(yōu)化(一)

“冷啟動(dòng)”與“熱啟動(dòng)”

主要區(qū)別:

名稱 區(qū)別
冷啟動(dòng) 啟動(dòng)時(shí),App的進(jìn)程不在系統(tǒng)里,需要開(kāi)啟新進(jìn)程。
熱啟動(dòng) 啟動(dòng)時(shí),App的進(jìn)程還在系統(tǒng)里,不需要開(kāi)啟新進(jìn)程

APP啟動(dòng)時(shí)間的優(yōu)化

那我們通常所說(shuō)的啟動(dòng)時(shí)間優(yōu)化都是在說(shuō)的冷啟動(dòng)的時(shí)間優(yōu)化

冷啟動(dòng)過(guò)程做了什么

主要分為三個(gè)階段:
1. pre-main:main()函數(shù)之前
2. main:main()函數(shù)之后(從main函數(shù)執(zhí)行,到設(shè)置self.window.rootViewController執(zhí)行完成)
3.首屏渲染完成后:(從self.window.rootViewController執(zhí)行完成到didFinishLaunchWithOptions方法作用域結(jié)束)

查看Pre-Main()階段花費(fèi)的總時(shí)間

想查看Pre-Main階段的時(shí)間比較簡(jiǎn)單。

直接打開(kāi)Xcode,找到Product->Scheme>Edit Scheme->Run->Arguments->Environment Variables->DYLD_PRINT_STATISTICS設(shè)置為 YES

image.png

Total pre-main time: 353.01 milliseconds (100.0%)
         dylib loading time: 210.68 milliseconds (59.6%)  //加載動(dòng)態(tài)庫(kù)
        rebase/binding time: 126687488.8 seconds (71548418.3%)
            ObjC setup time: 134.92 milliseconds (38.2%)
           initializer time: 137.90 milliseconds (39.0%)
           slowest intializers :
             libSystem.B.dylib :   3.83 milliseconds (1.0%)
   libBacktraceRecording.dylib :   7.19 milliseconds (2.0%)
    libMainThreadChecker.dylib :  97.57 milliseconds (27.6%)
        libLLVMContainer.dylib :  20.92 milliseconds (5.9%)

1 . load dylibs:這一階段dyld會(huì)分析應(yīng)用依賴的dylib,找到其mach-o文件,打開(kāi)和讀取這些文件并驗(yàn)證其有效性,接著會(huì)找到代碼簽名注冊(cè)到內(nèi)核,最后對(duì)dylib的每一個(gè)segment調(diào)用mmap()。
2 . rebase/bind:進(jìn)行rebase指針調(diào)整和bind符號(hào)綁定。
3 . ObjC setup:runtime運(yùn)行時(shí)初始化。包括ObjC相關(guān)Class的注冊(cè)、category注冊(cè)、selector唯一性檢查等。
4 . Initializers:調(diào)用每個(gè)ObjC類與分類的+load方法,調(diào)用attribute((constructor))修飾的函數(shù)、創(chuàng)建C++靜態(tài)全局變量。

main函數(shù)之前的啟動(dòng)過(guò)程

image.png
1. 加載dyld到App進(jìn)程

從Mach-o文件中讀取dylb的路徑并加載到App進(jìn)程

2. 加載動(dòng)態(tài)庫(kù)(包括所依賴的所有動(dòng)態(tài)庫(kù))

dyld會(huì)首先加載Mach-O中的 header 和 load command
接著就知道了這個(gè)app所依賴的動(dòng)態(tài)庫(kù)。添加依賴的動(dòng)態(tài)庫(kù)會(huì)按照先填加第一個(gè)依賴的動(dòng)態(tài)庫(kù)A,然后A所依賴的所有動(dòng)態(tài)庫(kù),這樣遞歸,直到所有的動(dòng)態(tài)庫(kù)添加完畢。通常一個(gè)App所依賴的動(dòng)態(tài)庫(kù)有100-400個(gè)。大多都是系統(tǒng)的動(dòng)態(tài)庫(kù)。它們會(huì)被緩存到dyld shared cache中,這樣大大提高了讀取效率。

查看mach-o文件所依賴的動(dòng)態(tài)庫(kù),可以通過(guò)MachOView的圖形化界面(展開(kāi)Load Command就能看到),也可以通過(guò)命令行otool。

otool -L xxxx 

xxxx:
    @rpath/PullToRefreshKit.framework/PullToRefreshKit (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1444.12.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    @rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 900.0.65)
    @rpath/libswiftCoreAudio.dylib (compatibility version 1.0.0, current version 900.0.65)
    //...
3. Rebase && Bind

有兩種主要的技術(shù)來(lái)保證應(yīng)用的安全:ASLRCode Sign

ASLR 的全稱是 Address space layout randomization,翻譯過(guò)來(lái)就是 “地址空間布局隨機(jī)化”。App被啟動(dòng)的時(shí)候,程序會(huì)被影射到邏輯的地址空間,這個(gè)邏輯的地址空間有一個(gè)起始地址,而ASLR技術(shù)使得這個(gè)起始地址是隨機(jī)的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函數(shù)的地址。
由于 ASLR 的存在,可執(zhí)行文件和動(dòng)態(tài)鏈接庫(kù)在虛擬內(nèi)存中的加載地址每次啟動(dòng)都不固定,所以需要這2步來(lái)修復(fù)鏡像中的資源指針,來(lái)指向正確的地址。

mach-o中有很多符號(hào),有指向當(dāng)前mach-o的,也有指向其他dylib的,比如printf。那么,在運(yùn)行時(shí),代碼如何準(zhǔn)確的找到printf的地址呢?

mach-o中采用了 PIC 技術(shù),全稱是Position Independ code。當(dāng)你的程序要調(diào)用printf的時(shí)候,會(huì)先在__DATA段中建立一個(gè)指針指向printf,在通過(guò)這個(gè)指針實(shí)現(xiàn)間接調(diào)用。dyld這時(shí)候需要做一些fix-up工作,即幫助應(yīng)用程序找到這些符號(hào)的實(shí)際地址。主要包括兩部分:

  • Rebase修復(fù)的是指向當(dāng)前鏡像內(nèi)部的資源指針

Rebase 修正內(nèi)部(指向當(dāng)前mach-o文件)的指針指向。是因?yàn)閯倓偺岬降腁SLR使得地址隨機(jī)化,導(dǎo)致起始地址不固定,另外由于Code Sign,導(dǎo)致不能直接修改Image。Rebase的時(shí)候只需要增加對(duì)應(yīng)的偏移量即可。待Rebase的數(shù)據(jù)都存放在__LINKEDIT中

  • Bind指向的是鏡像外部的資源指針。

Bind 修正外部指針指向。外部的符號(hào)引用則是由Bind解決。在解決Bind的時(shí)候,是根據(jù)字符串匹配的方式查找符號(hào)表,所以這個(gè)過(guò)程相對(duì)于Rebase來(lái)說(shuō)是略慢的

image.png

優(yōu)化該階段的關(guān)鍵在于減少__DATA segment中的指針數(shù)量。我們可以優(yōu)化的點(diǎn)有:

減少Objc類數(shù)量, 減少selector數(shù)量
減少C++虛函數(shù)數(shù)量
轉(zhuǎn)而使用swift struct(其實(shí)本質(zhì)上就是為了減少符號(hào)的數(shù)量)

4. ObjC setup
  • 讀取二進(jìn)制文件的 DATA 段內(nèi)容,找到與 objc 相關(guān)的信息
  • 注冊(cè) Objc 類,ObjC Runtime 需要維護(hù)一張映射類名與類的全局表。當(dāng)加載一個(gè) dylib 時(shí),其定義的所有的類都需要被注冊(cè)到這個(gè)全局表中;
  • 讀取 protocol 以及 category 的信息,把category的定義插入方法列表 (category registration),
  • 確保 selector 的唯一性
5. Initializers

接下來(lái)就是必要的初始化部分了,主要包括幾部分:

  1. Objc的+load()函數(shù)
  2. C++的構(gòu)造函數(shù)屬性函數(shù) 形如attribute((constructor)) void DoSomeInitializationWork()
  3. 非基本類型的C++靜態(tài)全局變量的創(chuàng)建。(通常是類或結(jié)構(gòu)體)(non-trivial initializer) 比如一個(gè)全局靜態(tài)結(jié)構(gòu)體的構(gòu)建,如果在構(gòu)造函數(shù)中有繁重的工作,那么會(huì)拖慢啟動(dòng)速度

這里要提一點(diǎn)的就是,+load方法已經(jīng)被棄用了,如果你用Swift開(kāi)發(fā),你會(huì)發(fā)現(xiàn)根本無(wú)法去寫(xiě)這樣一個(gè)方法,官方的建議是實(shí)用initialize。區(qū)別就是,load是在類裝載的時(shí)候執(zhí)行,而initialize是在類第一次收到message前調(diào)用。

所以,main()函數(shù)之前耗時(shí)的影響因素

  • 動(dòng)態(tài)庫(kù)加載越多,啟動(dòng)越慢。
  • ObjC類越多,啟動(dòng)越慢
  • C的constructor函數(shù)越多,啟動(dòng)越慢
  • C++靜態(tài)對(duì)象越多,啟動(dòng)越慢
  • ObjC的+load越多,啟動(dòng)越慢

pre-main的優(yōu)化

  1. 減少依賴不必要的庫(kù),不管是動(dòng)態(tài)庫(kù)還是靜態(tài)庫(kù);如果可以的話,把動(dòng)態(tài)庫(kù)改造成靜態(tài)庫(kù);如果必須依賴動(dòng)態(tài)庫(kù),則把多個(gè)非系統(tǒng)的動(dòng)態(tài)庫(kù)合并成一個(gè)動(dòng)態(tài)庫(kù);
  2. 檢查下 framework應(yīng)當(dāng)設(shè)為optional和required,如果該framework在當(dāng)前App支持的所有iOS系統(tǒng)版本都存在,那么就設(shè)為required,否則就設(shè)為optional,因?yàn)閛ptional會(huì)有些額外的檢查;
  3. 合并或者刪減一些OC類和函數(shù);關(guān)于清理項(xiàng)目中沒(méi)用到的類,使用工具AppCode代碼檢查功能,查到當(dāng)前項(xiàng)目中沒(méi)有用到的類(也可以用根據(jù)linkmap文件來(lái)分析,但是準(zhǔn)確度不算很高);有一個(gè)叫做FUI
    的開(kāi)源項(xiàng)目能很好的分析出不再使用的類,準(zhǔn)確率非常高,唯一的問(wèn)題是它處理不了動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)里提供的類,也處理不了C++的類模板。
  4. 移除不需要用到的, 減少selector、category的數(shù)量,如合并功能類似的類和Category
  5. 將不必須在+load方法中做的事情延遲到+initialize中,盡量不要用C++虛函數(shù)(創(chuàng)建虛函數(shù)表有開(kāi)銷)
  6. dispatch_once()代替所有的 attribute((constructor)) 函數(shù)、C++靜態(tài)對(duì)象初始化、ObjC的+load函數(shù);

減少調(diào)用 C/C++ 中的構(gòu)造器函數(shù)(用 attribute((constructor)) 修飾的函數(shù));

main()階段

主要工作就是初始化必要的服務(wù),顯示首頁(yè)內(nèi)容等。而我們的優(yōu)化也是圍繞如何能夠快速展現(xiàn)首頁(yè)來(lái)開(kāi)展。簡(jiǎn)要來(lái)說(shuō),只需要關(guān)注這個(gè)didFinishLaunchingWithOptions方法即可。
其實(shí)在這方法里面,我們主要是初始化第三方sdk,項(xiàng)目配置,設(shè)置根視圖控制器等。

查看Main()函數(shù)后的花費(fèi)時(shí)間

  1. 我們可以借助打點(diǎn)計(jì)時(shí)器BLStopwatch來(lái)度量didFinishLaunchingWithOptions每行代碼的初始時(shí)間。
例如:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
   NSLog(@"didFinishLaunchingWithOptions 開(kāi)始執(zhí)行");
   self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
   self.window.backgroundColor = [UIColor whiteColor];
   UITabBarController *tabVC = [[UITabBarController alloc] init];
   self.window.rootViewController =  tabVC;
   [self.window makeKeyAndVisible];
    
   BLStopwatch *timer = [BLStopwatch sharedStopwatch];
   [timer start];
   [self initShareSDK];
   [timer splitWithDescription:@"初始化分享SDK"];
   [timer stop];
   NSLog(@"%@",timer.prettyPrintedSplits);
   // #1 初始化SDK: 0.523  這是個(gè)假設(shè)時(shí)間
   return YES;
}
  1. 定時(shí)抓取主線程方法調(diào)用堆棧,計(jì)算一段時(shí)間里的方法耗時(shí)。(Xcode中的Time Profiler就是使用的這種的方法)
  2. 對(duì)objc_msgSend方法進(jìn)行hook,來(lái)得到所有方法的耗時(shí)。

注:hook是指在原有方法開(kāi)始執(zhí)行時(shí),換成你指定的方法(用RuntimeMethod Swizzle / Facebook開(kāi)源的fishhook框架)。或在原有方法的執(zhí)行前后,添加執(zhí)行你指定的方法。從而達(dá)到改變指定方法的目的。
(PS:關(guān)于fishhook,推薦閱讀一篇博客:fishhook原理

總結(jié)來(lái)說(shuō),就是必須一進(jìn)app就必須初始化和可以延遲初始化的。就上例而言,初始化一個(gè)SDK需要耗時(shí)0.5s,如果在該SDK 不是非必須初始化,可以放HomeVC的viewdidLoad或者viewDidAppear去做又或者需要用到才初始化。

* 日志、統(tǒng)計(jì)等必須在 APP 一啟動(dòng)就最先配置的事件
* 項(xiàng)目配置、環(huán)境配置、用戶信息的初始化 、推送、IM等事件
* 其他 SDK 和配置事件

main()優(yōu)化

1、展示的首頁(yè)盡量用純代碼創(chuàng)建,結(jié)合緩存更加。
2、結(jié)合BLStopwatch對(duì)啟動(dòng)服務(wù)進(jìn)行分級(jí)分時(shí)。
3、對(duì)一些非必要的初始化操作,可以放到viewDidAppear,因?yàn)榈絭iewDidAppear開(kāi)始執(zhí)行的時(shí)候,用戶已經(jīng)看到了APP的首屏,即宣告啟動(dòng)結(jié)束
4、一般僅針對(duì)測(cè)試版本進(jìn)行l(wèi)og打印
5、對(duì)didFinishLaunching里的函數(shù)考慮能否挖掘可以延遲加載或者懶加載
6、優(yōu)化主線程耗時(shí)操作,子線程執(zhí)行,防止屏幕卡頓。
7、對(duì)于啟動(dòng)頁(yè)的網(wǎng)絡(luò)請(qǐng)求接口可以做合并,減少請(qǐng)求

總之:性價(jià)比最高的優(yōu)化階段就是main函數(shù)之后的一些邏輯整理,盡量將不需要的耗時(shí)操作延遲到首屏展示之后執(zhí)行。

深入理解iOS App的啟動(dòng)過(guò)程

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

相關(guān)閱讀更多精彩內(nèi)容

  • 導(dǎo)語(yǔ) 本文介紹了如何優(yōu)化iOS App的啟動(dòng)性能。 本文分為四個(gè)部分: 第一部分科普了一些和App啟動(dòng)性能相關(guān)的前...
    2018火閱讀 1,077評(píng)論 0 0
  • 應(yīng)用啟動(dòng)流程 iOS應(yīng)用的啟動(dòng)可分為pre-main階段和main()階段,其中系統(tǒng)做的事情依次是: 1. pre...
    朽木自雕也閱讀 631評(píng)論 0 3
  • iOS App啟動(dòng)優(yōu)化 iOS啟動(dòng)可分為冷啟動(dòng)和熱啟動(dòng)兩種冷啟動(dòng):app為kill狀態(tài)下點(diǎn)擊app啟動(dòng)熱啟動(dòng):ap...
    zackwu閱讀 2,269評(píng)論 0 55
  • 夕陽(yáng)最美時(shí),也總是將近黃昏。世上有很多事都是這樣子的,尤其是一些特別輝煌美好的事。所以你不必傷感,也不用惋惜,縱然...
    二斤寂寞閱讀 957評(píng)論 0 6
  • 想做app啟動(dòng)優(yōu)化,當(dāng)然是先了解app啟動(dòng),什么時(shí)候開(kāi)始?什么時(shí)候結(jié)束?哪里是我們可以去優(yōu)化的地方? app啟動(dòng)開(kāi)...
    土豆趕著雞閱讀 481評(píng)論 0 1

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