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

應(yīng)用啟動(dòng)環(huán)節(jié),我們大致分為2種啟動(dòng):即冷啟動(dòng)(Cold Launch)和熱啟動(dòng)(Warm Launch),針對(duì)優(yōu)化,我們主要針對(duì)冷啟動(dòng)。

知識(shí)點(diǎn):打印啟動(dòng)時(shí)間

通過(guò)添加環(huán)境變量可以打印出APP的啟動(dòng)時(shí)間分析(Edit scheme -> Run -> Arguments)
DYLD_PRINT_STATISTICS設(shè)置為1
如果需要更詳細(xì)的信息,那就將DYLD_PRINT_STATISTICS_DETAILS設(shè)置為1

一般而言,大家把iOS冷啟動(dòng)的過(guò)程定義為:從用戶(hù)點(diǎn)擊App圖標(biāo)開(kāi)始到appDelegate didFinishLaunching方法執(zhí)行完成為止。這個(gè)過(guò)程主要分為兩個(gè)階段:

  • main()函數(shù)之前,即操作系統(tǒng)加載App可執(zhí)行文件到內(nèi)存,然后執(zhí)行一系列的加載&鏈接等工作,最后執(zhí)行至App的main()函數(shù)。

  • main()函數(shù)之后,即從main()開(kāi)始,到appDelegate的didFinishLaunchingWithOptions方法執(zhí)行完畢。

然而,當(dāng)didFinishLaunchingWithOptions執(zhí)行完成時(shí),用戶(hù)還沒(méi)有看到App的主界面,也不能開(kāi)始使用App。例如在外賣(mài)App中,App還需要做一些初始化工作,然后經(jīng)歷定位、首頁(yè)請(qǐng)求、首頁(yè)渲染等過(guò)程后,用戶(hù)才能真正看到數(shù)據(jù)內(nèi)容并開(kāi)始使用,我們認(rèn)為這個(gè)時(shí)候冷啟動(dòng)才算完成。

冷啟動(dòng)過(guò)程圖解

在調(diào)用main()函數(shù)之前,基本所有的工作都是由操作系統(tǒng)完成的,開(kāi)發(fā)者能夠插手的地方不多,所以如果想要優(yōu)化這段時(shí)間,就必須先了解一下,操作系統(tǒng)在main()之前做了什么。main()之前操作系統(tǒng)所做的工作就是把可執(zhí)行文件(Mach-O格式)加載到內(nèi)存空間,然后加載動(dòng)態(tài)鏈接庫(kù)dyld,再執(zhí)行一系列動(dòng)態(tài)鏈接操作和初始化操作的過(guò)程(加載、綁定、及初始化方法)。這方面的資料網(wǎng)上比較多,但重復(fù)性較高,此處附上一篇WWDC的Topic:Optimizing App Startup Time

dyld

'dyld(dynamic link editor),Apple的動(dòng)態(tài)鏈接器,可以用來(lái)裝載Mach-O文件(可執(zhí)行文件、動(dòng)態(tài)庫(kù)等)

啟動(dòng)APP時(shí),dyld所做的事情有:
裝載APP的可執(zhí)行文件,同時(shí)會(huì)遞歸加載所有依賴(lài)的動(dòng)態(tài)庫(kù)
當(dāng)dyld把可執(zhí)行文件、動(dòng)態(tài)庫(kù)都裝載完畢后,會(huì)通知Runtime進(jìn)行下一步的處理

Dyld各階段內(nèi)容

具體關(guān)于dyld的講解推薦dyld專(zhuān)題文章講的很細(xì)致。

Runtime 運(yùn)行時(shí)

啟動(dòng)APP時(shí),runtime所做的事情有

調(diào)用map_images進(jìn)行可執(zhí)行文件內(nèi)容的解析和處理
在load_images中調(diào)用call_load_methods,調(diào)用所有Class和Category的+load方法
進(jìn)行各種objc結(jié)構(gòu)的初始化(注冊(cè)O(shè)bjc類(lèi) 、初始化類(lèi)對(duì)象等等)
調(diào)用C++靜態(tài)初始化器和attribute((constructor))修飾的函數(shù)

到此為止,可執(zhí)行文件和動(dòng)態(tài)庫(kù)中所有的符號(hào)(Class,Protocol,Selector,IMP,…)都已經(jīng)按格式成功加載到內(nèi)存中,被runtime 所管理

了解完main()之前的加載過(guò)程后,我們可以分析出一些影響mian函數(shù)之前消耗時(shí)間的因素:

  • 動(dòng)態(tài)庫(kù)加載越多,啟動(dòng)越慢。
  • ObjC類(lèi),方法越多,啟動(dòng)越慢。
  • ObjC的+load越多,啟動(dòng)越慢。
  • C的constructor函數(shù)越多,啟動(dòng)越慢。
  • C++靜態(tài)對(duì)象越多,啟動(dòng)越慢。

代碼瘦身

隨著業(yè)務(wù)的迭代,不斷有新的代碼加入,同時(shí)也會(huì)廢棄掉無(wú)用的代碼和資源文件,但是工程中經(jīng)常有無(wú)用的代碼和文件被遺棄在角落里,沒(méi)有及時(shí)被清理掉。這些無(wú)用的部分一方面增大了App的包體積,另一方便也拖慢了App的冷啟動(dòng)速度,所以及時(shí)清理掉這些無(wú)用的代碼和資源十分有必要。

通過(guò)對(duì)Mach-O文件的了解,可以知道__TEXT:__objc_methname:中包含了代碼中的所有方法,而__DATA__objc_selrefs中則包含了所有被使用的方法的引用,通過(guò)取兩個(gè)集合的差集就可以得到所有未被使用的代碼。核心方法如下,具體可以參考:objc_cover:

 def referenced_selectors(path):
    re_sel = re.compile("__TEXT:__objc_methname:(.+)") //獲取所有方法
    refs = set()
    lines = os.popen("/usr/bin/otool -v -s __DATA __objc_selrefs %s" % path).readlines() # ios & mac //真正被使用的方法
    for line in lines:
        results = re_sel.findall(line)
        if results:
            refs.add(results[0])
    return refs

+load優(yōu)化

目前iOS App中或多或少的都會(huì)寫(xiě)一些+load方法,用于在App啟動(dòng)執(zhí)行一些操作,+load方法在Initializers階段被執(zhí)行,但過(guò)多+load方法則會(huì)拖慢啟動(dòng)速度,對(duì)于大中型的App更是如此。通過(guò)對(duì)App中+load的方法分析,發(fā)現(xiàn)很多代碼雖然需要在App啟動(dòng)時(shí)較早的時(shí)機(jī)進(jìn)行初始化,但并不需要在+load這樣非??壳暗奈恢?,完全是可以延遲到App冷啟動(dòng)后的某個(gè)時(shí)間節(jié)點(diǎn),例如一些路由操作。

優(yōu)化耗時(shí)操作

在main()之后主要工作是各種啟動(dòng)項(xiàng)的執(zhí)行,主界面的構(gòu)建,例如TabBarVC,HomeVC等等。資源的加載,如圖片I/O、圖片解碼、archive文檔等。這些操作中可能會(huì)隱含著一些耗時(shí)操作,靠單純閱讀非常難以發(fā)現(xiàn),如何發(fā)現(xiàn)這些耗時(shí)點(diǎn)呢?找到合適的工具就會(huì)事半功倍。

Time Profiler

Time Profiler是Xcode自帶的時(shí)間性能分析工具,它按照固定的時(shí)間間隔來(lái)跟蹤每一個(gè)線程的堆棧信息,通過(guò)統(tǒng)計(jì)比較時(shí)間間隔之間的堆棧狀態(tài),來(lái)推算某個(gè)方法執(zhí)行了多久,并獲得一個(gè)近似值。Time Profiler的使用方法網(wǎng)上有很多使用教程,這里我們也不過(guò)多介紹,附上一篇使用文檔:Instruments Tutorial with Swift: Getting Started。

火焰圖

除了Time Profiler,火焰圖也是一個(gè)分析CPU耗時(shí)的利器,相比于Time Profiler,火焰圖更加清晰。火焰圖分析的產(chǎn)物是一張調(diào)用棧耗時(shí)圖片,之所以稱(chēng)為火焰圖,是因?yàn)檎麄€(gè)圖形看起來(lái)就像一團(tuán)跳動(dòng)的火焰,火焰尖部是調(diào)用棧的棧頂,底部是棧底,縱向表示調(diào)用棧的深度,橫向表示消耗的時(shí)間。一個(gè)格子的寬度越大,越說(shuō)明其可能是瓶頸。分析火焰圖主要就是看那些比較寬大的火苗,特別留意那些類(lèi)似“平頂山”的火苗。

列舉幾點(diǎn)具體優(yōu)化的方向


優(yōu)化方向

閃屏業(yè)務(wù)上優(yōu)化點(diǎn)

現(xiàn)在許多App在啟動(dòng)時(shí)并不直接進(jìn)入首頁(yè),而是會(huì)向用戶(hù)展示一個(gè)持續(xù)一小段時(shí)間的閃屏頁(yè),如果使用恰當(dāng),這個(gè)閃屏頁(yè)就能幫我們節(jié)省一些啟動(dòng)時(shí)間。因?yàn)楫?dāng)一個(gè)App比較復(fù)雜的時(shí)候,啟動(dòng)時(shí)首次構(gòu)建App的UI就是一個(gè)比較耗時(shí)的過(guò)程,假定這個(gè)時(shí)間是0.2秒,如果我們是先構(gòu)建首頁(yè)UI,然后再在Window上加上這個(gè)閃屏頁(yè),那么冷啟動(dòng)時(shí),App就會(huì)實(shí)實(shí)在在地卡住0.2秒,但是如果我們是先把閃屏頁(yè)作為App的RootViewController,那么這個(gè)構(gòu)建過(guò)程就會(huì)很快。因?yàn)殚W屏頁(yè)只有一個(gè)簡(jiǎn)單的ImageView,而這個(gè)ImageView則會(huì)向用戶(hù)展示一小段時(shí)間,這時(shí)我們就可以利用這一段時(shí)間來(lái)構(gòu)建首頁(yè)UI了,一舉兩得。
TOGO途歌共享車(chē)客戶(hù)端就是使用的這種方案。

對(duì)于快速迭代的App,隨著業(yè)務(wù)復(fù)雜度的增加,冷啟動(dòng)時(shí)長(zhǎng)會(huì)不可避免的增加。冷啟動(dòng)流程也是一個(gè)比較復(fù)雜的過(guò)程,當(dāng)遇到冷啟動(dòng)性能瓶頸時(shí),我們可以根據(jù)App自身的特點(diǎn),配合工具的使用,從多方面、多角度進(jìn)行優(yōu)化。同時(shí),優(yōu)化冷啟動(dòng)存量問(wèn)題只是冷啟動(dòng)治理的第一步,因?yàn)槔鋯?dòng)性能問(wèn)題并不是一日造成的,也不能簡(jiǎn)單的通過(guò)一次優(yōu)化工作就能解決,我們需要通過(guò)合理的設(shè)計(jì)、規(guī)范的約束,來(lái)有效地管控性能問(wèn)題的增量,并通過(guò)持續(xù)的線上監(jiān)控來(lái)及時(shí)發(fā)現(xiàn)并修正性能問(wèn)題,這樣才能夠長(zhǎng)期保證良好的App冷啟動(dòng)體驗(yà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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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