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函數執行前總共花費的多長時間。
(2)查看加載了多少動態庫
在Product->Scheme->Edit Scheme->Run->Diagnostics->Logging->勾選Dynamic Library Loads,就可以在控制臺中查看本項目中加載的所有動態庫(包括系統的和自己的)。
(3)查看Main函數啟動后的耗時
main函數調用后的耗時,可以使用一些工具來監控,有一種非常笨但是很實用的方法,就是通過打點,在didFinishLaunchingWithOptions開始前打一個點,在App顯示完成第一個界面再打一個點,計算兩個點之間的耗時,就可以知道main函數調用后到界面顯示出來的耗時了,但是這樣只能籠統的知道總的耗時,并不能準確的知道時間花在了哪里。
如果想用這個打點法的話,推薦一個打點工具BLStopwatch
如果想準確知道時間都花在了哪里,推薦使用下面兩種方法。
4. 監控App啟動耗時,精準找出時間都花在了哪里,方便逐一優化
準確監控方法有兩種:
定時抓取主線程上的方法調用堆棧,計算一段時間里各個方法的耗時。Xcode自帶的Time Profiler就是用的這種方法。
對objc_msgSend方法進行hook來掌握所有方法的執行耗時。
根據這兩種方法,分別實現兩個工具,來監控耗時
由于能力有限,我只根據第一種方法做出來一個計算某個線程的耗時工具,放在了這里BSMonitorTimeTool,大致思路如下:
(1). 通過定時器,每隔0.01s,獲取一次主線程的函數堆棧,將函數名稱、函數地址、函數耗時模型化為TimeModel
,保存在callStackDict
中,其中key為函數地址,value為TimeModel
(2). 定時執行的回調中,每次都判斷函數地址是否存在,如果已經存在此函數地址,就講對應的TimeModel中的耗時增加0.01s;如果不存在此函數地址,就初始化一個TimeModel,并將時間設置為0.01s。
(3). 當主界面顯示完成之后,輸出此callStackDict
,即可查看主線程中每個方法的耗時
5. 歡迎大家指正錯誤,希望能夠共同進步
本文章是參考了很多大佬的文章,歡迎各位前去膜拜
- 戴銘大佬的極客時間02
- 貝聊科技大佬的一次立竿見影的啟動時間優化