背景
之前有收到用戶反饋 App 的啟動(dòng)時(shí)間較長(zhǎng),在和市面上大部分 App 啟動(dòng)時(shí)間相比后,確實(shí)發(fā)現(xiàn) App 啟動(dòng)較慢,于是開始分析項(xiàng)目中導(dǎo)致啟動(dòng)時(shí)間變長(zhǎng)的原因,并對(duì)啟動(dòng)時(shí)間進(jìn)行優(yōu)化。
現(xiàn)狀分析
一般而言,啟動(dòng)時(shí)間指用戶從點(diǎn)擊 App 那一刻開始到看到 App 第一個(gè)頁(yè)面之間消耗的時(shí)間。
蘋果將啟動(dòng)時(shí)間分為兩部分:pre-main 的時(shí)間和 main() 之后的時(shí)間(當(dāng)然還有我們?nèi)藶榧由先サ拈W屏顯示時(shí)間)。
pre-main時(shí)間:即調(diào)用 main() 函數(shù)之前的加載時(shí)間,在這段時(shí)間里系統(tǒng)會(huì)進(jìn)行加載動(dòng)態(tài)庫(kù)、注冊(cè) Objc 類等系統(tǒng)操作。
main() 之后的時(shí)間:即從調(diào)用 main( ) 函數(shù)到看到第一個(gè)頁(yè)面之間的時(shí)間(從 main 函數(shù)開始到第一個(gè)頁(yè)面的 - viewDidAppear 被調(diào)用)。
統(tǒng)計(jì)結(jié)果
以下為各個(gè)機(jī)型啟動(dòng)時(shí)間的統(tǒng)計(jì)結(jié)果,由于冷啟動(dòng)的啟動(dòng)時(shí)間受系統(tǒng)影響波動(dòng)較大,啟動(dòng)時(shí)間均測(cè)試5次以上取平均值,統(tǒng)計(jì)時(shí)間均不包含閃屏廣告頁(yè)的時(shí)間(單位為秒):
| iPhone 8 Plus | iPhone 6s Plus | iPhone SE | |
|---|---|---|---|
| pre-main 時(shí)間 | 0.879 | 0.869 | 0.958 |
| main之后的時(shí)間 | 1.622 | 1.762 | 1.885 |
| 總時(shí)間 | 2.501 | 2.631 | 2.843 |
以下為main之后時(shí)間的統(tǒng)計(jì)結(jié)果:
Launch時(shí)間:main 開始到 didFinishLaunchingWithOptions 結(jié)束的時(shí)間
首頁(yè)渲染時(shí)間:didFinishLaunchingWithOptions 結(jié)束到 RootViewController 的 - viewDidAppear 被調(diào)用的時(shí)間
| iPhone 8 Plus | iPhone 6s Plus | iPhone SE | |
|---|---|---|---|
| Launch時(shí)間 | 0.701 | 0.740 | 0.849 |
| 首頁(yè)渲染時(shí)間 | 0.921 | 1.022 | 1.036 |
| 總時(shí)間 | 1.622 | 1.762 | 1.885 |
經(jīng)過(guò)統(tǒng)計(jì) pre-main 的時(shí)間基本穩(wěn)定在 0.8s-0.9s 左右,Launch 時(shí)間穩(wěn)定在 0.8s 左右,首頁(yè)渲染時(shí)間穩(wěn)定在1s左右,均存在優(yōu)化空間。
優(yōu)化方案
pre-main 階段優(yōu)化
以下為 iPhone 6s Plus 正常啟動(dòng)消耗的pre-main時(shí)間(蘋果提供了內(nèi)建的測(cè)量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環(huán)境變量 DYLD_PRINT_STATISTICS 設(shè)為 1):
Total pre-main time: 866.86 milliseconds (100.0%)
dylib loading time: 328.28 milliseconds (37.8%)
rebase/binding time: 49.19 milliseconds (5.6%)
ObjC setup time: 62.85 milliseconds (7.2%)
initializer time: 426.38 milliseconds (49.1%)
slowest intializers :
libSystem.B.dylib : 7.52 milliseconds (0.8%)
libMainThreadChecker.dylib : 37.19 milliseconds (4.2%)
libglInterpose.dylib : 61.17 milliseconds (7.0%)
libMTLInterpose.dylib : 22.23 milliseconds (2.5%)
MyMoney : 392.50 milliseconds (45.2%)
pre-main時(shí)間主要由 4 部分組成:
-
dylib loading:
這一階段 dyld 會(huì)分析應(yīng)用依賴的 dylib ,所以,依賴的 dylib 越少越好。在這一步,我們能做的優(yōu)化就是檢查是否存在不需要的 dylib ,移除不必要的 dylib 。
在項(xiàng)目?jī)?yōu)化實(shí)踐中,我們移除了一個(gè)沒(méi)有必要的動(dòng)態(tài)庫(kù),并將幾個(gè)動(dòng)態(tài)庫(kù)合成為一個(gè)動(dòng)態(tài)庫(kù),減少動(dòng)態(tài)庫(kù)數(shù)量。
-
rebase/binding:
這一階段系統(tǒng)主要注冊(cè) Objc 類。所以,指針數(shù)量越少越好。這一步能做的優(yōu)化有:
清理項(xiàng)目中無(wú)用的類
刪減沒(méi)有被調(diào)用到或者已經(jīng)廢棄的方法
刪減一些無(wú)用的靜態(tài)變量
可通過(guò) AppCode 等工具掃描項(xiàng)目中未使用的代碼。
-
Objc srtup:
這一階段沒(méi)有什么特別能優(yōu)化的地方,如果 rebase/binding 階段優(yōu)化的好這步耗時(shí)也會(huì)很少。
-
initializer:
這一階段,dyld 開始運(yùn)行程序的初始化函數(shù),調(diào)用每個(gè) Objc 類和分類的 +load 方法,調(diào)用 C/C++ 中的構(gòu)造器函數(shù)。initializer階段執(zhí)行完后,dyld 開始調(diào)用 main() 函數(shù)。在這一步,檢查 +load 方法,盡量把事情推遲到 +initiailize 方法里執(zhí)行。
在這里我們修改了部分原本代碼中直接在 +load 函數(shù)初始化邏輯改為在 +initialize 中加載,也就是到使用時(shí)才加載。
main()函數(shù)之后的優(yōu)化
didFinishLaunchingWithOptions 優(yōu)化
思路:目前 App 的 didFinishLaunchingWithOptions 方法里執(zhí)行了幾十項(xiàng)業(yè)務(wù),有一大部分業(yè)務(wù)并不是一定要在這里執(zhí)行的,如支付配置、客服配置、分享配置等。整理該方法里的業(yè)務(wù),能延遲加載的就往后推遲,防止其影響啟動(dòng)時(shí)間。
通過(guò)打點(diǎn)計(jì)時(shí)器統(tǒng)計(jì)各個(gè)業(yè)務(wù)的耗時(shí)時(shí)間(iPhone 6s Plus):

整理 didFinishLaunchingWithOptions ,將業(yè)務(wù)分級(jí),對(duì)于非必須的業(yè)務(wù)移到首頁(yè)顯示后加載。同時(shí),為了防止以后新加的業(yè)務(wù)繼續(xù)往 didFinishLaunchingWithOptions 里扔,可以新建一個(gè)類負(fù)責(zé)啟動(dòng)事件,新加的業(yè)務(wù)可以往這邊添加。
首頁(yè)渲染優(yōu)化
- 減少啟動(dòng)期間創(chuàng)建的 UIViewController 數(shù)量
通過(guò)打符號(hào)斷點(diǎn)-[UIViewController viewDidLoad]發(fā)現(xiàn) App 啟動(dòng)過(guò)程中創(chuàng)建了 12 個(gè) UIViewController(包括閃屏),即在啟動(dòng)過(guò)程中創(chuàng)建了 12 個(gè)視圖控制器,導(dǎo)致首頁(yè)渲染時(shí)間較長(zhǎng)。
- 延遲首頁(yè)耗時(shí)操作
App 首頁(yè)有個(gè)側(cè)滑頁(yè)面及側(cè)滑手勢(shì),并且該頁(yè)面是用 xib 構(gòu)建的,將該 ViewController 改為代碼構(gòu)建,同時(shí)延遲該頁(yè)面的創(chuàng)建時(shí)機(jī),等首頁(yè)顯示后再創(chuàng)建該頁(yè)面及側(cè)滑手勢(shì),這個(gè)改動(dòng)節(jié)省了 300-400ms。
- 去除啟動(dòng)時(shí)沒(méi)必要及不合理的操作
項(xiàng)目中使用了自定義的側(cè)滑返回,在每次 push 的時(shí)候都會(huì)截圖,啟動(dòng)的時(shí)候自定義導(dǎo)航欄會(huì)截取兩張多余首頁(yè)的圖片,并且截圖用的 API (renderInContext) 性能較差,耗時(shí) 800ms 左右,去掉啟動(dòng)截圖的操作。
閃屏請(qǐng)求回調(diào)里寫plist文件的操作放在主線程,導(dǎo)致啟動(dòng)時(shí)占用主線程,將文件讀寫移到子線程操作。
閃屏優(yōu)化
閃屏在啟動(dòng)的時(shí)候也占據(jù)了很長(zhǎng)的時(shí)間,合理的閃屏顯示邏輯同樣能大大的減少用戶的等待時(shí)間。
閃屏顯示通常都是有一個(gè)倒計(jì)時(shí),倒計(jì)時(shí)結(jié)束后顯示首頁(yè)。通常倒計(jì)時(shí)都是使用 NSTimer ,且每秒倒計(jì)時(shí)結(jié)束都需要修改頁(yè)面上的文字,這些操作必須在主線程做,并且 NSTimer 依賴于主線程的 runloop 狀態(tài),主線程阻塞會(huì)導(dǎo)致定時(shí)器不準(zhǔn)。所以為了保證閃屏倒計(jì)時(shí)的正常顯示,首頁(yè)是在閃屏顯示結(jié)束后才去創(chuàng)建的。
主線程工作狀態(tài)如下:

這種情況下用戶等待的時(shí)間是從“創(chuàng)建閃屏”到“創(chuàng)建、顯示首頁(yè)”結(jié)束的時(shí)間。
由于閃屏顯示時(shí)間較長(zhǎng)(通常都有幾秒),用戶在等待閃屏結(jié)束的這段時(shí)間如果用來(lái)創(chuàng)建首頁(yè)內(nèi)容肯定是夠的,于是就考慮能不能先顯示閃屏,在閃屏倒計(jì)時(shí)的同時(shí)去創(chuàng)建、顯示首頁(yè)。
在將首頁(yè)創(chuàng)建、顯示邏輯提前后發(fā)現(xiàn)并沒(méi)有那么簡(jiǎn)單,就像前面提到的閃屏倒計(jì)時(shí)使用 NSTimer,NSTimer 又依賴于 runloop,且每秒倒計(jì)時(shí)結(jié)束都必須修改文字,這些操作必須依賴于主線程。只是把首頁(yè)創(chuàng)建邏輯提前就導(dǎo)致閃屏倒計(jì)時(shí)不準(zhǔn)確,主要表現(xiàn)在第一秒格外的長(zhǎng),因?yàn)殚W屏顯示后主線程就去做創(chuàng)建首頁(yè)的工作(其實(shí)還包括之前被移到首頁(yè)顯示后才啟動(dòng)的服務(wù),也會(huì)在這時(shí)候一起被主線程處理),必須等首頁(yè)內(nèi)容創(chuàng)建完成后 NSTimer 的倒計(jì)時(shí)才會(huì)觸發(fā),也必須等首頁(yè)內(nèi)容創(chuàng)建完成后才能修改文字。
這時(shí)候的主線程工作狀態(tài)如下:

這時(shí)用戶等待的時(shí)間并沒(méi)有明顯縮短,而且還出現(xiàn)了倒計(jì)時(shí)不準(zhǔn)的 bug 。
出現(xiàn)上面這種情況主要是由兩個(gè)原因?qū)е碌模?/p>
使用的定時(shí)器依賴于主線程。
修改文字操作依賴于主線程。
第一個(gè)問(wèn)題比較容易解決,不使用 NSTimer,改用 GCD 定時(shí)器放在其他線程即可??墒堑诙€(gè)問(wèn)題是更新 UI ,蘋果爸爸明確表示更新 UI 必須放在主線程,一時(shí)好像就無(wú)解了,但是真的無(wú)解嗎?
回過(guò)頭再看一下需求,其實(shí)我們要做的就是每秒修改用戶看到的文字,并且不依賴于主線程,不依賴于主線程第一時(shí)間就想到了動(dòng)畫,iOS 動(dòng)畫是在另一個(gè)進(jìn)程(BackBoard 進(jìn)程)實(shí)現(xiàn)的,主線程阻塞是不會(huì)影響 iOS 動(dòng)畫的,利用這一特性就能完美的解決這個(gè)問(wèn)題。我們將要每秒要顯示的文字提前生成圖片,然后使用生成的圖片數(shù)組做動(dòng)畫,這樣在閃屏顯示期間主線程就可以去做其他事情,保證閃屏顯示時(shí)間的同時(shí)減少了用戶的等待時(shí)間。
優(yōu)化后的工作狀態(tài):

總結(jié)
對(duì)于啟動(dòng)時(shí)間優(yōu)化其實(shí)就是遵循一個(gè)原則:盡早讓用戶看到首頁(yè)內(nèi)容。根據(jù)這一原則將一些非必須的操作盡量往后移,通常是移到首頁(yè)顯示后執(zhí)行,同時(shí)對(duì)于無(wú)法往后移的操作,盡可能不占用主線程,主線程盡量只做 UI 操作,將其他操作移到子線程(或者像上述動(dòng)畫一樣移到其他進(jìn)程)。