iOS - 優化App冷啟動速度

1. App的啟動分為三個主要階段:

  • main()函數執行前

  • main()函數執行后(從main函數執行,到設置self.window.rootViewController)

  • 首屏渲染完成后(從設置self.window.rootViewController到didFinishLaunchWithOptions方法作用域結束)

main函數執行前,系統會做的事情:
  • 加載可執行文件(App的.o文件集合)

  • 加載動態鏈接庫,進行rebase指針調整和bind符號綁定

  • Objc運行時的初始處理,包括Objc相關類注冊、category注冊、selector唯一性檢查等

  • 初始化,包括了執行+load()方法、attribute((constructor))修飾的函數的調用、創建C++靜態全局變量。

main()函數執行后:

main()函數執行后的階段,指的是從main()函數執行開始,到appDelegate的didFinishLaunchingWithOpentions方法里首屏渲染相關方法執行完成。

這里應該是從功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是App啟動必要的初始化功能,哪些是只需要在對應功能開始使用時才需要初始化的,將這些放到各自合適的階段執行。

首屏渲染完成后:

首屏渲染后的這個階段,指的是didFinishLaunchWithOptions方法作用域內執行首屏渲染之后的所有方法執行完成,即從 設置了self.window.rootViewController開始 到 didFinishLaunchWithOptions方法作用域 結束。

首屏渲染完成后用戶就可以看到App的首頁信息了,把這個階段內卡住主線程的方法解決掉就可以了。

注解:
  • App啟動后,首先加載可執行文件,然后加載dyld,然后加載所有依賴庫,然后調用所有的+load(),然后調用main(),然后調用UIApplicationMain(),然后調用AppDelegate的代理didFinishLaunchWithOptions.

  • 可執行文件是指Mach-O格式的文件,也就是App中所有.o文件的集合體,從這里可以獲取dyld的路徑,然后加載dyld。

  • dyld是指蘋果的動態鏈接器,加載dyld后,就會去初始化運行環境,開啟緩存策略,加載依賴庫,并且會調用每一個依賴庫的初始化方法,包括RunTime也是在這里被初始化的,當所有的依賴庫都被初始化完成后,RunTime會對項目中所有的類進行類初始化,調用所有的+load()方法,最后dyld會返回main函數地址,然后main函數會被調用。

  • 知曉上述的流程后,我們就明白為什么優化啟動速度,要去減少動態庫加載,要少用+load(),理論明白了之后,我們就要看看具體怎么做了。

  • 動態庫是指可以共享的代碼文件、資源文件、頭文件等的打包集合體。在Xcode->Targets->General->Link Binary With Libraries可以檢查自己的庫,

  • 減少+load()的使用,將里面的內容放到渲染結束后去做,或者用+initialize()代替。+load()方法在main()調用前就會調用,而+initialize()方法是在類第一次收到消息后,才會調用,兩者的區別可以參考這里

    Main函數調用前

    Main函數調用后

2.具體優化方法

(1)減少+load()的使用

使用+initialize()的方法代替+load(),注意把邏輯移動到+initialize()時,要注意避免+initialize()的重復調用問題,可以使用dispatch_once()讓邏輯只執行一次。

(2)對多個動態庫進行合并

蘋果公司建議使用更少的動態庫,并且建議在使用動態庫的數量較多時,盡量將多個動態庫進行合并。數量上,蘋果公司最多可以支持6個非系統動態庫合并為一個。

(3)優化類、方法、全局變量

減少加載啟動后不會去使用的類或方法;控制C++全局變量的數量

(4)功能級別的啟動優化

main()開始執行后到首屏渲染完成前,只處理首屏相關的業務,其他的都放到首屏渲染完成后去做。

(5)方法級別的啟動優化

首先檢查首屏渲染完成前主線程上的耗時操作,將沒必要的操作滯后或異步。通常耗時操作有:加載、編輯、存儲圖片和文件等資源。

3. 查看耗時

(1)查看Main()調用前花費的總時間

在Product->Scheme->Edit Scheme->Run->Arguments->Environment Variables->DYLD_PRINT_STATISTICS設置為YES,就可以在控制臺中查看main函數執行前總共花費的多長時間。

設置環境變量.png

控制臺會輸出pre-main的總時間.png

(2)查看加載了多少動態庫

在Product->Scheme->Edit Scheme->Run->Diagnostics->Logging->勾選Dynamic Library Loads,就可以在控制臺中查看本項目中加載的所有動態庫(包括系統的和自己的)。


image.png
(3)查看Main函數啟動后的耗時

main函數調用后的耗時,可以使用一些工具來監控,有一種非常笨但是很實用的方法,就是通過打點,在didFinishLaunchingWithOptions開始前打一個點,在App顯示完成第一個界面再打一個點,計算兩個點之間的耗時,就可以知道main函數調用后到界面顯示出來的耗時了,但是這樣只能籠統的知道總的耗時,并不能準確的知道時間花在了哪里。

如果想用這個打點法的話,推薦一個打點工具BLStopwatch

如果想準確知道時間都花在了哪里,推薦使用下面兩種方法。

4. 監控App啟動耗時,精準找出時間都花在了哪里,方便逐一優化

準確監控方法有兩種:
  1. 定時抓取主線程上的方法調用堆棧,計算一段時間里各個方法的耗時。Xcode自帶的Time Profiler就是用的這種方法。

  2. 對objc_msgSend方法進行hook來掌握所有方法的執行耗時。

根據這兩種方法,分別實現兩個工具,來監控耗時

由于能力有限,我只根據第一種方法做出來一個計算某個線程的耗時工具,放在了這里BSMonitorTimeTool,大致思路如下:

(1). 通過定時器,每隔0.01s,獲取一次主線程的函數堆棧,將函數名稱、函數地址、函數耗時模型化為TimeModel,保存在callStackDict中,其中key為函數地址,value為TimeModel

(2). 定時執行的回調中,每次都判斷函數地址是否存在,如果已經存在此函數地址,就講對應的TimeModel中的耗時增加0.01s;如果不存在此函數地址,就初始化一個TimeModel,并將時間設置為0.01s。

(3). 當主界面顯示完成之后,輸出此callStackDict,即可查看主線程中每個方法的耗時

5. 歡迎大家指正錯誤,希望能夠共同進步

本文章是參考了很多大佬的文章,歡迎各位前去膜拜

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容