內(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à)
能夠給用戶帶來(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)用戶重啟手機(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_STATISTICS
和DYLD_PRINT_STATISTICS_DETAILS
訪問(wèn)。安裝操作系統(tǒng)時(shí)候就可用了仍侥,但它打印了很多內(nèi)部調(diào)試信息要出,并沒(méi)有什么用。它缺少了某些你可能想知道的信息∨┰ǎ現(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)境變量,如下圖所示:
運(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毫秒:
上面的基本步驟就是前面理論部分講的啟動(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)化方案:
- 使用更少的dylib;
- 合并現(xiàn)有的dylib鸠真;
- 使用靜態(tài)存檔悯仙;
- 懶加載;
(二)重設(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
類符號(hào),很可能你有很多ObjC
類融师。所以你能做的一件事就是減少ObjC
類對(duì)象和ivars
的數(shù)量右钾。有很多編碼樣式都鼓勵(lì)只有一個(gè)或兩個(gè)函數(shù)的小類,這些特殊的模式可能會(huì)導(dǎo)致速度逐漸變慢诬滩。當(dāng)你越加越多時(shí)霹粥,更要格外小心。現(xiàn)在有100或者1000個(gè)類不成問(wèn)題疼鸟,但有些大型應(yīng)用有上萬(wàn)個(gè)類后控。在這種情況下,將會(huì)消耗更多的啟動(dòng)時(shí)間空镜,因?yàn)閮?nèi)核要把它們讀入頁(yè)面浩淘。
還可以做一件事情捌朴,可以嘗試減少使用C++虛擬函數(shù)。虛擬函數(shù)將會(huì)創(chuà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ù) - 類,選擇器和類別邮旷; - 減少C++虛擬函數(shù)黄选;
- 使用Swift結(jié)構(gòu);
- 檢查機(jī)器生成的代碼 - 使用偏移量而非指針婶肩,標(biāo)記為只讀办陷;
(三)ObjC Setup
優(yōu)化
關(guān)于設(shè)置ObjC
,前面理論部分講過(guò)它做的工作律歼,它要處理類的注冊(cè)民镜,要處理非脆弱ivar
,要處理分類的注冊(cè)险毁,還要讓選擇器唯一制圈。這里我們不用處理太多们童,因?yàn)檫@些問(wèn)題通過(guò)之前對(duì)重設(shè)基址,數(shù)據(jù)鲸鹦,和綁定的修復(fù)時(shí)都已經(jīng)解決慧库,之前所做的減少和在這里做的完全相同。
(四)初始化器的優(yōu)化
初始化器有兩種類型馋嗜,顯示初始化器齐板,比如+load
,前面理論部分建議用+initiailize
取代它葛菇,這將導(dǎo)致ObjC
運(yùn)行時(shí)在類被實(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++全局變量∽宀伲可以選擇調(diào)用site initializers
取代它苛坚,在很多地方可以把全局變量替換成想要初始化的無(wú)全局結(jié)構(gòu)或指針的對(duì)象。還有一種選擇是沒(méi)有非默認(rèn)初始化器色难。所以在C++中泼舱,初始化器稱為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)啟線程,也是出于同樣的理由窖维。