App啟動(dòng)優(yōu)化 - 實(shí)踐一

內(nèi)容概要:
1. 啟動(dòng)速度
2. 如何測(cè)量啟動(dòng)時(shí)間
3. 影響啟動(dòng)時(shí)間的原因
4. 啟動(dòng)優(yōu)化的方案

我們的應(yīng)用在運(yùn)行前應(yīng)該減少操作,推遲一些啟動(dòng)行為,從而在啟動(dòng)前一點(diǎn)點(diǎn)時(shí)間進(jìn)行初始化。下面讓我們來(lái)看本章內(nèi)容概要。

一、啟動(dòng)速度

在不同平臺(tái)上,應(yīng)用的啟動(dòng)時(shí)間有所不同。蘋(píng)果在開(kāi)發(fā)者大會(huì)中提出400毫秒是一個(gè)不錯(cuò)的啟動(dòng)時(shí)間。原因在于,當(dāng)你看著應(yīng)用在運(yùn)行時(shí),手機(jī)上的啟動(dòng)動(dòng)畫(huà)
能夠給用戶(hù)帶來(lái)一種在主屏幕和應(yīng)用之間切換時(shí)的持續(xù)感。這些動(dòng)畫(huà)占用時(shí)間,并且會(huì)給你一個(gè)機(jī)會(huì)隱藏啟動(dòng)時(shí)間。
顯然根據(jù)情況會(huì)有所不同,app擴(kuò)展程序也是應(yīng)用啟動(dòng)的一部分,它們啟動(dòng)的時(shí)間不同。手機(jī),電視和手表是不同的設(shè)備,但400毫秒是一個(gè)很好的啟動(dòng)目標(biāo)。此外,啟動(dòng)時(shí)間不要超過(guò)20秒,如果超過(guò)20秒,OS會(huì)終止應(yīng)用,以為它進(jìn)入了死循環(huán)。
最后在所支持的最慢的設(shè)備上進(jìn)行測(cè)試也很重要,在Apple平臺(tái)支持的所有設(shè)備上,這些時(shí)間都是常量值。如果你在iPhone 6s上測(cè)試的結(jié)果達(dá)到400毫秒,很可能在iPhone 5上達(dá)不到。在前面的理論部分我們知道啟動(dòng)時(shí)需要做什么,要解析圖像、映射圖像、重設(shè)基址圖像、綁定圖像、啟動(dòng)圖像初始化器、調(diào)用主函數(shù),還有一些操作,包括運(yùn)行框架初始化器以及加載NIB,最終在應(yīng)用委托里收到回調(diào)。最后兩個(gè)操作也計(jì)算在我們前面說(shuō)的400毫秒的時(shí)間里。
啟動(dòng)應(yīng)用時(shí),分冷啟動(dòng)和熱啟動(dòng)。

  • 熱啟動(dòng)是指啟動(dòng)時(shí)應(yīng)用已經(jīng)在內(nèi)存里,或者因?yàn)閱?dòng)過(guò),之前退出了,但還在內(nèi)核的磁盤(pán)緩存里,或者因?yàn)槟銊偘阉鼜?fù)制過(guò)去。
  • 冷啟動(dòng)是指啟動(dòng)時(shí)應(yīng)用不在磁盤(pán)緩存里。

測(cè)量冷啟動(dòng)時(shí)間通常更為重要。冷啟動(dòng)時(shí)間更為中重要的原因是,當(dāng)用戶(hù)重啟手機(jī)后啟動(dòng)應(yīng)用,或很長(zhǎng)時(shí)間后啟動(dòng)應(yīng)用,這時(shí)非常需要一個(gè)快速啟動(dòng)。為了測(cè)量冷啟動(dòng)時(shí)間,必須在每次測(cè)量之間重啟設(shè)備。如果你正嘗試優(yōu)化熱啟動(dòng)時(shí)間,那冷啟動(dòng)時(shí)間應(yīng)該也會(huì)隨之加快。你可以通過(guò)加速開(kāi)發(fā)周期加快熱啟動(dòng),但是請(qǐng)時(shí)不時(shí)地測(cè)試一下冷啟動(dòng)。

二、如何測(cè)量啟動(dòng)時(shí)間

在主函數(shù)啟動(dòng)之前該如何測(cè)量時(shí)間?dyld里有內(nèi)置的測(cè)量系統(tǒng),可以通過(guò)設(shè)置環(huán)境變量DYLD_PRINT_STATISTICSDYLD_PRINT_STATISTICS_DETAILS訪問(wèn)。安裝操作系統(tǒng)時(shí)候就可用了,但它打印了很多內(nèi)部調(diào)試信息,并沒(méi)有什么用。它缺少了某些你可能想知道的信息?,F(xiàn)在我們就來(lái)改進(jìn),在新的OS里進(jìn)步顯著。
它可以為你提供更為相關(guān)的信息,這些信息應(yīng)該會(huì)提供可操作的方法,來(lái)加快啟動(dòng)時(shí)間。當(dāng)加載每一個(gè)dylib,調(diào)試程序必須暫停啟動(dòng)才能解析應(yīng)用的符號(hào)和加載斷點(diǎn),這通過(guò)USB線將非常耗時(shí)。但是dyld清楚這一點(diǎn),它把調(diào)試時(shí)間從注冊(cè)時(shí)間里減出去,所以不必為此擔(dān)心。但是你會(huì)注意到它,因?yàn)?code>dyld會(huì)顯示比你在鐘表中所觀察到的數(shù)字精細(xì)得多。這是預(yù)期的和能夠接受的,如果你看到了那個(gè)數(shù)字,一切都是正確的。這里只是提示下。
在Xcode里設(shè)置環(huán)境變量,如下圖所示:

image_00

運(yùn)行后,控制臺(tái)的輸出信息如下:

Total pre-main time: 10.6 seconds (100.0%)
         dylib loading time:  240.09 milliseconds (2.2%)
        rebase/binding time:  351.29 milliseconds (3.3%)
            ObjC setup time:  11.83 milliseconds (0.1%)
           initializer time:  10 seconds (94.3%)
       slowest intializers :
              MyAwesomeApp :  10.0 seconds (94.2%)

下面的時(shí)間條代表上面不同部分所占時(shí)間,而白色的虛線代表400毫秒:

image_01

上面的基本步驟就是前面理論部分講的啟動(dòng)順序。

三、優(yōu)化方案

(一)dylib加載優(yōu)化

關(guān)于dylib加載,還有從中看到的速度緩慢,需特別了解的是嵌入式dyld會(huì)非常昂貴。我們知道一個(gè)應(yīng)用大概包含100到400個(gè)Dylib,但是操作系統(tǒng)的dylib很快,這是因?yàn)闃?gòu)建操作系統(tǒng)時(shí),我們預(yù)計(jì)算了大量dylib的數(shù)據(jù)。但是我們?cè)陂_(kāi)發(fā)操作系統(tǒng)時(shí),無(wú)法做到每個(gè)應(yīng)用里的每個(gè)dylib。我們無(wú)法預(yù)計(jì)算你要嵌入應(yīng)用的dylib,所以加載時(shí)必須要經(jīng)過(guò)一個(gè)耗時(shí)的過(guò)程。其解決方案是少用dylib,而這將非常困難。這并不是說(shuō)完全不能使用,有很多方法可以合并已有的dylib。
可以使用靜態(tài)存檔,把dylib用這種方法鏈接到應(yīng)用。還可以使用延遲加載,也就是使用dlopen()函數(shù)。但是dlopen()函數(shù)會(huì)帶來(lái)細(xì)微的性能和正確性的問(wèn)題,實(shí)際上會(huì)導(dǎo)致之后做更多的工作量,而這些工作量被延遲執(zhí)行了。所以這是一個(gè)可行的選項(xiàng),但是必須要仔細(xì)思考清楚,盡量減少這種延遲加載的操作。

優(yōu)化方案:

  1. 使用更少的dylib;
  2. 合并現(xiàn)有的dylib;
  3. 使用靜態(tài)存檔;
  4. 懶加載;
image_02
(二)重設(shè)基址和綁定優(yōu)化

重設(shè)基址和綁定需要350毫秒時(shí)間,根據(jù)前面的理論部分我們知道,重設(shè)基址由于I/O會(huì)更慢一些,而綁定在計(jì)算上會(huì)昂貴,但它已經(jīng)完成I/O。所以I/O是為了它們,它們混合在一起,時(shí)間也混合在一起。我們深入研究一下,就會(huì)發(fā)現(xiàn)時(shí)間消耗在修復(fù)DATA段里的指針。所以我們必須減少指針的修復(fù)。用其他工具可以看到在DATA,分區(qū),dyld信息中修復(fù)的指針。還能顯示正在哪些段和分區(qū)操作,你會(huì)很清楚地了解到在修復(fù)什么。比如,若看到一個(gè)在ObjC分區(qū)的ObjC類(lèi)符號(hào),很可能你有很多ObjC類(lèi)。所以你能做的一件事就是減少ObjC類(lèi)對(duì)象和ivars的數(shù)量。有很多編碼樣式都鼓勵(lì)只有一個(gè)或兩個(gè)函數(shù)的小類(lèi),這些特殊的模式可能會(huì)導(dǎo)致速度逐漸變慢。當(dāng)你越加越多時(shí),更要格外小心?,F(xiàn)在有100或者1000個(gè)類(lèi)不成問(wèn)題,但有些大型應(yīng)用有上萬(wàn)個(gè)類(lèi)。在這種情況下,將會(huì)消耗更多的啟動(dòng)時(shí)間,因?yàn)閮?nèi)核要把它們讀入頁(yè)面。
還可以做一件事情,可以嘗試減少使用C++虛擬函數(shù)。虛擬函數(shù)將會(huì)創(chuàng)建我們稱(chēng)之為V表格的東西,這和ObjC元數(shù)據(jù)相同,因?yàn)樗鼈冊(cè)?code>DATA段創(chuàng)建了必須修復(fù)的結(jié)構(gòu)。它們比ObjC元數(shù)據(jù)小,但它們對(duì)于某些應(yīng)用程序來(lái)說(shuō)仍然很重要。
還可以使用Swift的結(jié)構(gòu)體。因?yàn)镾wift通常使用更少這種帶有指針修復(fù)的數(shù)據(jù)。并且Swift更為內(nèi)聯(lián),可以更好的使用code-gen減少消耗。所以轉(zhuǎn)為Swift語(yǔ)言也是一個(gè)好方法。
還有一點(diǎn),需要小心機(jī)器生成的代碼。曾經(jīng)有過(guò)這樣的例子,你可能用DSL或一些自定義語(yǔ)言描述某個(gè)結(jié)構(gòu),然后有一個(gè)程序從中生成其他代碼。如果這些程序中有很多指針,它們將變得非常昂貴,因?yàn)樯纱a時(shí)會(huì)生成非常非常大的結(jié)構(gòu)。也有生成兆量級(jí)數(shù)據(jù)的情況。但好處是比較容易進(jìn)行控制,因?yàn)槟阒恍枰淖兇a生成器,使其使用非指針的內(nèi)容,比如偏移基址,結(jié)構(gòu)。

優(yōu)化方案:

  • 減少__DATA指針;
  • 減少ObjC元數(shù)據(jù) - 類(lèi),選擇器和類(lèi)別;
  • 減少C++虛擬函數(shù);
  • 使用Swift結(jié)構(gòu);
  • 檢查機(jī)器生成的代碼 - 使用偏移量而非指針,標(biāo)記為只讀;
image_03
(三)ObjC Setup優(yōu)化

關(guān)于設(shè)置ObjC,前面理論部分講過(guò)它做的工作,它要處理類(lèi)的注冊(cè),要處理非脆弱ivar,要處理分類(lèi)的注冊(cè),還要讓選擇器唯一。這里我們不用處理太多,因?yàn)檫@些問(wèn)題通過(guò)之前對(duì)重設(shè)基址,數(shù)據(jù),和綁定的修復(fù)時(shí)都已經(jīng)解決,之前所做的減少和在這里做的完全相同。

image_04

(四)初始化器的優(yōu)化

初始化器有兩種類(lèi)型,顯示初始化器,比如+load,前面理論部分建議用+initiailize取代它,這將導(dǎo)致ObjC運(yùn)行時(shí)在類(lèi)被實(shí)例化而不是文件被加載時(shí)初始化代碼。
或者在C/C++里,有一個(gè)可以放在函數(shù)上的屬性,可以讓函數(shù)像初始化器一樣生成代碼,因此這是顯示初始化器,但不建議這么做。建議選擇調(diào)用site initializers取代上面的方式。調(diào)用site initializers是指調(diào)用像dispatch_once()函數(shù),或者在跨平臺(tái)代碼里的pthread_once(),或者在C++代碼中的std::once()。所有這些函數(shù)基本上都有相同的功能,這些函數(shù)的代碼只會(huì)在第一次點(diǎn)擊時(shí)運(yùn)行一次。dispatch_once()在系統(tǒng)里很優(yōu)秀,第一次執(zhí)行以后,幾乎等同于無(wú)操作,直接跳過(guò)。所以強(qiáng)烈建議不要使用顯示初始化器。
另一種初始化器是隱式初始化器。隱式初始化器大部分來(lái)自帶有非默認(rèn)初始化器,非默認(rèn)構(gòu)造函數(shù)的C++全局變量??梢赃x擇調(diào)用site initializers取代它,在很多地方可以把全局變量替換成想要初始化的無(wú)全局結(jié)構(gòu)或指針的對(duì)象。還有一種選擇是沒(méi)有非默認(rèn)初始化器。所以在C++中,初始化器稱(chēng)為POD,一個(gè)普通的舊數(shù)據(jù)。
如果對(duì)象只是普通的舊數(shù)據(jù),靜態(tài)鏈接器,或者靜態(tài)鏈接器將會(huì)為DATA分區(qū)預(yù)計(jì)算所有的數(shù)據(jù),只把數(shù)據(jù)放在那里,不一定要運(yùn)行,不一定要修復(fù)。最后一點(diǎn),很難找到它,因?yàn)樗鼈兪请[性的,但是編譯器會(huì)收到警告 --- -Wglobal-constructors,如果這么做,只要產(chǎn)生其中一個(gè),就會(huì)有警告。所以把它添加到編譯器使用的標(biāo)志里是個(gè)好方法。還有一個(gè)選擇就是使用Swift重新編寫(xiě)。理由就是Swift有全局變量,并且會(huì)被初始化,它們確保在使用前被初始化,但是其方法不是用初始化器,在后臺(tái)使用一次dispatch_once()。所以轉(zhuǎn)為Swift將會(huì)做到這一點(diǎn)。
最后在初始化器里請(qǐng)不要調(diào)用dlopen()函數(shù),它將會(huì)帶來(lái)巨大的性能問(wèn)題。原因有很多,dyld在運(yùn)行時(shí),是在應(yīng)用啟動(dòng)之前,我們可以做一些諸如關(guān)閉鎖的操作,因?yàn)槭菃尉€程。當(dāng)dlopen()出現(xiàn)在那種情況下,初始化器的運(yùn)行發(fā)生了改變,可能會(huì)有多線程,必須要打開(kāi)鎖,將會(huì)帶來(lái)巨大的性能下降,還會(huì)帶來(lái)細(xì)微的死鎖和未定義的行為。還有,不要在初始化器上開(kāi)啟線程,也是出于同樣的理由。

九、參考資料

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

相關(guān)閱讀更多精彩內(nèi)容

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