背景
項目功能越來越復(fù)雜化,影響到啟動時間(主要是感覺自己菜,需要學(xué)習(xí))
APP啟動時都干了什么事
一般情況下,App 的啟動分為冷啟動和熱啟動,我們這里說的是優(yōu)化冷啟動時間。
冷啟動是指, App 點擊啟動前,它的進程不在系統(tǒng)里,需要系統(tǒng)新創(chuàng)建一個進程分配給它啟動的情況。這是一次完整的啟動過程。
熱啟動是指 ,App 在冷啟動后用戶將 App 退后臺,在 App 的進程還在系統(tǒng)里的情況下,用戶重新啟動進入 App 的過程,這個過程做的事情非常少。
一般而言,App 的啟動時間,指的是從用戶點擊 App 開始,到用戶看到第一個界面之間的時間。總結(jié)來說,App 的啟動主要包括三個階段:
main() 函數(shù)執(zhí)行前;
main() 函數(shù)執(zhí)行后;
首屏渲染完成后。
1.main() 函數(shù)執(zhí)行前
main() 函數(shù)執(zhí)行前的階段啟動時間通過添加環(huán)境變量,可以打印出APP的啟動時間分析:
(Edit scheme -->Run -->Arguments -->Environment Variables)
添加一項:Name為DYLD_PRINT_STATISTICS,Value為1,然后運行項目查看日志
Total pre-main time: 348.75 milliseconds (100.0%)
dylib loading time: 302.06 milliseconds (86.6%)
rebase/binding time: 126687488.9 seconds (135339814.9%)
ObjC setup time: 17.65 milliseconds (5.0%)
initializer time: 50.90 milliseconds (14.5%)
slowest intializers :
libSystem.B.dylib : 5.88 milliseconds (1.6%)
libMainThreadChecker.dylib : 32.44 milliseconds (9.3%)
在 main() 函數(shù)執(zhí)行前,系統(tǒng)主要會做下面幾件事情(對照上面代碼看):
- 加載可執(zhí)行文件(App 的.o 文件的集合);
- 加載動態(tài)鏈接庫,進行 rebase 指針調(diào)整和 bind 符號綁定;
- Objc 運行時的初始處理,包括 Objc 相關(guān)類的注冊、category 注冊、selector 唯一性檢查等;
- 初始化,包括了執(zhí)行 +load() 方法、attribute((constructor)) 修飾的函數(shù)的調(diào)用、創(chuàng)建 C++ 靜態(tài)全局變量。
對應(yīng)的啟動速度優(yōu)化:
- 減少動態(tài)庫加載。每個庫本身都有依賴關(guān)系,蘋果公司建議使用更少的動態(tài)庫,并且建議在使用動態(tài)庫的數(shù)量較多時,盡量將多個動態(tài)庫進行合并。數(shù)量上,蘋果公司最多可以支持 6 個非系統(tǒng)動態(tài)庫合并為一個。
- 移除沒有用到的類或者方法。
- +load() 方法的內(nèi)容可以放到首屏渲染完成后再執(zhí)行,或使用 +initialize() 方法替換掉。因為,在一個 +load() 方法里,進行運行時方法替換操作會帶來 4 毫秒的消耗。
- 控制 C++ 全局變量的數(shù)量。
2.main() 函數(shù)執(zhí)行后啟動時間
main() 函數(shù)執(zhí)行后的階段,指的是從 main() 函數(shù)執(zhí)行開始,到 appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相關(guān)方法執(zhí)行完成。
首頁的業(yè)務(wù)代碼都是要在這個階段,也就是首屏渲染前執(zhí)行的,主要包括了:
- 首屏初始化所需配置文件的讀寫操作;
- 首屏列表大數(shù)據(jù)的讀??;
- 首屏渲染的大量計算等。
很多時候,開發(fā)者會把各種初始化工作都放到這個階段執(zhí)行,導(dǎo)致渲染完成滯后。更加優(yōu)化的開發(fā)方式,應(yīng)該是從功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 啟動必要的初始化功能,而哪些是只需要在對應(yīng)功能開始使用時才需要初始化的。梳理完之后,將這些初始化功能分別放到合適的階段進行。例如地圖,支付,統(tǒng)計,推送等功能的注冊,有些是必須放在appDelegate中注冊,有些可以放在首屏渲染完成之后注冊,或者有些可以放在子線程注冊。
3.首屏渲染完成后
首屏渲染后的這個階段,主要完成的是,非首屏其他業(yè)務(wù)服務(wù)模塊的初始化、監(jiān)聽的注冊、配置文件的讀取等。從函數(shù)上來看,這個階段指的就是截止到 didFinishLaunchingWithOptions 方法作用域內(nèi)執(zhí)行首屏渲染之后的所有方法執(zhí)行完成。
這個階段就是從渲染完成時開始,到 didFinishLaunchingWithOptions 方法作用域結(jié)束時結(jié)束。這個階段用戶已經(jīng)能夠看到 App 的首頁信息了,所以優(yōu)化的優(yōu)先級排在最后。但是,那些會卡住主線程的方法還是需要最優(yōu)先處理的,不然還是會影響到用戶后面的交互操作。
功能級別的啟動優(yōu)化
功能級別的啟動優(yōu)化,就是要從 main() 函數(shù)執(zhí)行后這個階段下手。
優(yōu)化的思路是:main() 函數(shù)開始執(zhí)行后到首屏渲染完成前只處理首屏相關(guān)的業(yè)務(wù),其他非首屏業(yè)務(wù)的初始化、監(jiān)聽注冊、配置文件讀取等都放到首屏渲染完成后去做。
其他小優(yōu)化
- 合并功能相似的類和擴展(Category)
由于Category的實現(xiàn)原理,和ObjC的動態(tài)綁定有很強的關(guān)系,實際上類的擴展是比較占用啟動時間的。盡量可能合并一些擴展,并不是讓你不使用擴展 - 清理無用的資源圖片,壓縮較大的資源圖片
推薦使用LSUnusedResources查找無用的資源圖片,使用ImageOptim壓縮較大的資源圖片 - 優(yōu)化applicationWillFinishLaunching
需要在applicationWillFinishLaunching里處理的業(yè)務(wù)較多時,可以管理起這些任務(wù)
將不需要馬上在applicationWillFinishLaunching執(zhí)行的代碼延后執(zhí)行 - 優(yōu)化rootViewController
rootViewController的加載,適當(dāng)將某一級的childViewController或subviews延后加載
如果你的App可能會被后臺拉起并冷啟動,可考慮不加載rootViewController - 不使用xib,直接使用代碼加載首頁視圖
- NSUserDefaults實際上是在Library文件夾下會生產(chǎn)一個plist文件,如果文件太大的話一次能讀取到內(nèi)存中可能很耗時,如果耗時很大的話需要拆分(需考慮老版本覆蓋安裝兼容問題)
- 每次用NSLog方式打印會隱式的創(chuàng)建一個Calendar,因此需要刪減啟動時各業(yè)務(wù)方打的log,或者僅僅針對內(nèi)測版輸出log
- 梳理應(yīng)用啟動時發(fā)送的所有網(wǎng)絡(luò)請求,是否可以統(tǒng)一在異步線程請求
方法級別啟動優(yōu)化
檢查首屏渲染完成前主線程上的耗時方法,將非剛需的耗時方法滯后或異步執(zhí)行。耗時較長的方法主要發(fā)生在計算大量數(shù)據(jù)的情況下,例如加載、編輯、存儲圖片和文件等資源。
比如+load() 方法,一個耗時 4 毫秒,100 個就是 400 毫秒。ReactiveCocoa 框架每創(chuàng)建一個信號都有 6 毫秒的耗時。這樣,稍不注意各種信號的創(chuàng)建就都被放在了首屏渲染完成前,進而導(dǎo)致 App 的啟動速度大幅變慢等。
目前來看,對 App 啟動速度的監(jiān)控,主要有兩種手段。
第一種方法是,定時抓取主線程上的方法調(diào)用堆棧,計算一段時間里各個方法的耗時。
Xcode 工具套件里自帶的 Time Profiler ,采用的就是這種方式。開發(fā)類似工具成本不高,能夠快速開發(fā)后集成到你的 App 中,以便在真實環(huán)境中進行檢查。
對于定時時間間隔的控制
- 間隔長:會漏掉某些方法,從而導(dǎo)致檢查出來的耗時不精確
- 間隔短:抓取堆棧這個方法本身調(diào)用過多會影響整體耗時,導(dǎo)致結(jié)果不準(zhǔn)確
- 定時抓取主線程調(diào)用棧的方式精準(zhǔn)度不夠高,做參考足以
- 大神得出最合適時間為 0.01 秒,雖然導(dǎo)致許多運行速度快的方法監(jiān)控誤差,對整體耗時影響小
第二種方法是,對 objc_msgSend 方法進行 hook 來掌握所有方法的執(zhí)行耗時。
Hook:在原方法開始執(zhí)行時換成執(zhí)行其他指定的方法,或者在原有方法執(zhí)行前后執(zhí)行你指定的方法,來達到掌握和改變指定方法的目的。
hook objc_msgSend 這種方式的優(yōu)點是非常精確,而缺點是只能針對 Objective-C 的方法。對c方法和block需要借助第三方框架處理,編寫維護成本高。
檢查方法耗時的工具
推薦使用工具SMCallTrace
友情提示:需要在SMCallTrace.m中打開第54行的注釋。
自主實現(xiàn)時間檢測工具
學(xué)習(xí)筆記,參考多位大神,感謝大神分享!