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

背景

之前有收到用戶反饋 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 部分組成:

  1. 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ù)量。

  2. 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)目中未使用的代碼。

  3. Objc srtup:

    這一階段沒(méi)有什么特別能優(yōu)化的地方,如果 rebase/binding 階段優(yōu)化的好這步耗時(shí)也會(huì)很少。

  4. 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):

各個(gè)業(yè)務(wù)耗時(shí)時(shí)間

整理 didFinishLaunchingWithOptions ,將業(yè)務(wù)分級(jí),對(duì)于非必須的業(yè)務(wù)移到首頁(yè)顯示后加載。同時(shí),為了防止以后新加的業(yè)務(wù)繼續(xù)往 didFinishLaunchingWithOptions 里扔,可以新建一個(gè)類負(fù)責(zé)啟動(dòng)事件,新加的業(yè)務(wù)可以往這邊添加。

首頁(yè)渲染優(yōu)化
  1. 減少啟動(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)。

  1. 延遲首頁(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。

  1. 去除啟動(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)程)。

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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