通過(guò)閱讀這篇文章,我們將了解APP啟動(dòng)過(guò)程中都做了哪些事情擂送。文章分為三部分悦荒,第一部分是原理講解,第二部分是優(yōu)化方案嘹吨,第三部分是實(shí)踐應(yīng)用搬味。
第一部分 原理講解
APP啟動(dòng)的分類(lèi)
APP分為冷啟動(dòng)和熱啟動(dòng)。
冷啟動(dòng)是指蟀拷, App 點(diǎn)擊啟動(dòng)前碰纬,它的進(jìn)程不在系統(tǒng)里,需要系統(tǒng)新創(chuàng)建一個(gè)進(jìn)程分配給它啟動(dòng)的情況问芬。這是一次完整的啟動(dòng)過(guò)程悦析。
熱啟動(dòng),對(duì)于熱啟動(dòng)的理解存在分歧此衅,一種是是指戶點(diǎn)擊home鍵后再次回到前臺(tái)顯示强戴,還原到退出前的狀態(tài),繼續(xù)為用戶服務(wù)炕柔;另一種是用戶殺掉APP進(jìn)程后在dyld沒(méi)有刪除緩存的情況下重啟APP酌泰,這種情況下的啟動(dòng)速度會(huì)比冷啟動(dòng)要快一些。
對(duì)于熱啟動(dòng)的分歧匕累,筆者覺(jué)得從優(yōu)化角度來(lái)說(shuō)陵刹,第二種根據(jù)有研究的意義和符合實(shí)際使用情況。對(duì)比兩個(gè)啟動(dòng)方式對(duì)用戶的影響欢嘿,冷啟動(dòng)的快慢會(huì)給用戶造成第一印象的好壞衰琐,影響用戶的使用體驗(yàn)和留存也糊。接下來(lái)我們深入研究下APP冷啟動(dòng)過(guò)程。
從點(diǎn)擊到啟動(dòng)
當(dāng)用戶點(diǎn)擊手機(jī)桌面上的圖標(biāo)到首頁(yè)展示到用戶面前并且可以進(jìn)行交互羡宙,這個(gè)過(guò)程我們定義為APP啟動(dòng)的一個(gè)完整過(guò)程狸剃。這個(gè)過(guò)程中發(fā)生了很多事情。系統(tǒng)先讀取App的可執(zhí)行文件(Mach-O文件)狗热,從里面獲得dyld的路徑钞馁,然后加載dyld,dyld去初始化運(yùn)行環(huán)境匿刮,開(kāi)啟緩存策略僧凰,加載程序相關(guān)依賴庫(kù)(其中也包含我們的可執(zhí)行文件),并對(duì)這些庫(kù)進(jìn)行鏈接熟丸,最后調(diào)用每個(gè)依賴庫(kù)的初始化方法训措,在這一步,runtime被初始化光羞。當(dāng)所有依賴庫(kù)的初始化后绩鸣,輪到最后一位(程序可執(zhí)行文件)進(jìn)行初始化,在這時(shí)runtime會(huì)對(duì)項(xiàng)目中所有類(lèi)進(jìn)行類(lèi)結(jié)構(gòu)初始化纱兑,然后調(diào)用所有的load方法呀闻。最后dyld返回main函數(shù)地址,main函數(shù)被調(diào)用萍启,我們便來(lái)到了熟悉的程序入口总珠。
我們將以上階段分為Ready、Pre-Main勘纯、Main三個(gè)階段。
Ready階段
iPhone/iPad的桌面系統(tǒng)也是一個(gè)應(yīng)用钓瞭,我們稱之為Springboard驳遵,當(dāng)用戶觸碰屏幕點(diǎn)擊APP的icon時(shí),XNU加載Mach-O和dyld山涡,然后系統(tǒng)會(huì)從XNU內(nèi)核態(tài)將控制權(quán)轉(zhuǎn)移到dyld用戶態(tài)堤结。dyld會(huì)負(fù)責(zé)后續(xù)工作,這時(shí)第一個(gè)階段完成鸭丛。
為了精簡(jiǎn)文章體積和側(cè)重本文重點(diǎn)竞穷,我們簡(jiǎn)單了解幾個(gè)概念:Mach-O 、dyld ,有興趣的同學(xué)可以自行深入學(xué)習(xí)和研究鳞溉。
Mach-O
Mach-O是Mach object的縮寫(xiě)瘾带,是Mac\iOS上用于存儲(chǔ)程序、庫(kù)的標(biāo)準(zhǔn)格式熟菲。
Mach-O有以下幾種類(lèi)型看政,其實(shí)動(dòng)態(tài)庫(kù)朴恳、靜態(tài)庫(kù)、程序本身允蚣、bundle文件其實(shí)都是屬于Mach-O文件于颖。
既然是文件,就會(huì)有固定的存儲(chǔ)格式:Header嚷兔、Load Commands森渐、Raw Data三大部分。
Header:保存了一些基本信息冒晰,包括了該文件運(yùn)行的平臺(tái)章母、文件類(lèi)型、LoadCommands的個(gè)數(shù)等等翩剪。
LoadCommands:可以理解為加載命令乳怎,在加載Mach-O文件時(shí)會(huì)使用這里的數(shù)據(jù)來(lái)確定內(nèi)存的分布以及相關(guān)的加載命令。比如我們的main函數(shù)的加載地址前弯,程序所需的dyld的文件路徑蚪缀,以及相關(guān)依賴庫(kù)的文件路徑。
Raw Data: 這里包含了具體的代碼恕出、數(shù)據(jù)等等询枚。
dyld
dyld是動(dòng)態(tài)鏈接器。dyld從可執(zhí)行文件的依賴開(kāi)始, 遞歸加載所有的依賴動(dòng)態(tài)鏈接庫(kù)浙巫。系統(tǒng)內(nèi)核在加載動(dòng)態(tài)庫(kù)前金蜀,會(huì)加載dyld,然后調(diào)用去執(zhí)行__dyld_start()的畴,該函數(shù)會(huì)執(zhí)行dyldbootstrap::start()渊抄,后者會(huì)執(zhí)行_main()函數(shù),dyld的加載動(dòng)態(tài)庫(kù)的代碼就是從_main()開(kāi)始執(zhí)行的丧裁。
dyld存放位置目前版本存放的路徑是/usr/lib/dyld护桦。系統(tǒng)會(huì)解析Mach-O文件中的Load Commands段的LC_LOAD_DYLINKER,獲取到dyld的路徑(如下圖)煎娇。
根據(jù)這一階段的具體工作二庵,我們沒(méi)有優(yōu)化空間,準(zhǔn)備工作完全有系統(tǒng)來(lái)處理缓呛。
pre-main階段
當(dāng)dyld執(zhí)行到_main()函數(shù)時(shí)催享,它將加載程序所需要的動(dòng)態(tài)庫(kù),對(duì)其進(jìn)行rebase以及bind操作哟绊,然后運(yùn)行初始化函數(shù)因妙,執(zhí)行程序的main函數(shù)。
我們將以上分為L(zhǎng)oad dylibs image、Rebase image兰迫、Bind image信殊、Objc、setup initializers五個(gè)階段汁果,接下來(lái)我們看一下每個(gè)階段都做了什么工作涡拘。
-
Load dylibs image階段
將Mach-O中的代碼段和數(shù)據(jù)段加載到虛擬內(nèi)存后,dyld從主執(zhí)行文件的 header 獲取到需要加載的所依賴動(dòng)態(tài)庫(kù)列表据德,找到對(duì)應(yīng)的dylib后確保它是Mach-O文件鳄乏,接著找到代碼簽名并將其注冊(cè)到內(nèi)核中。應(yīng)用所依賴的 dylib 文件可能會(huì)再依賴其他 dylib棘利,所以 dyld 所需要加載的是動(dòng)態(tài)庫(kù)列表一個(gè)遞歸依賴的集合橱野。一般應(yīng)用會(huì)加載 100 到 400 個(gè) dylib 文件,但大部分都是系統(tǒng) dylib善玫,它們會(huì)被預(yù)先計(jì)算和緩存起來(lái)水援,加載速度很快。(下圖是一個(gè)APP的加載動(dòng)態(tài)庫(kù)的情況茅郎,在lldb下通過(guò)image命令查看)
動(dòng)態(tài)庫(kù)的加載分為兩種類(lèi)型蜗元,一種是系統(tǒng)動(dòng)態(tài)庫(kù),一種是自定義的動(dòng)態(tài)庫(kù)系冗。針對(duì)系統(tǒng)動(dòng)態(tài)庫(kù)奕扣,蘋(píng)果幫我們做了很好的優(yōu)化,從iOS3.1開(kāi)始掌敬,為了提高性能惯豆,絕大部分的系統(tǒng)動(dòng)態(tài)庫(kù)文件都打包存放到了一個(gè)緩存文件中,那就是動(dòng)態(tài)共享緩存dsc(dyld shared cache)奔害,dsc存在的位置在/System/Library/Caches/com.apple.dyld/dyld_shared_cache_armX,X代表的是ARM處理器指令集架構(gòu)楷兽,APP啟動(dòng)前會(huì)通過(guò)dyld在des中查找然后遞歸加載所有依賴的動(dòng)態(tài)庫(kù)到內(nèi)存中。(dyld源碼下載地址https://opensource.apple.com/tarballs/dyld/)
對(duì)于自定義動(dòng)態(tài)庫(kù)的使用舀武,我們有很大的優(yōu)化空間拄养,減少自定義動(dòng)態(tài)庫(kù)的使用數(shù)量,體積和引用關(guān)系银舱,都有助于縮短這個(gè)階段的耗時(shí)。
-
Rebase image階段
為了防止程序被輕易的惡意篡改跛梗,iOS4.3后蘋(píng)果提出了ASLR的方案寻馏,每個(gè)程序有自己的虛擬內(nèi)存,所有的虛擬內(nèi)存都是從0x00000000開(kāi)始的核偿,使用ASLR后诚欠,程序加載VM Address會(huì)有一個(gè)偏移量,這個(gè)偏移量是每次首次加載程序時(shí)隨機(jī)產(chǎn)生,然后在這個(gè)偏移量的基礎(chǔ)上繼續(xù)加載Pagezero區(qū)域(0x100000000)轰绵,之后的內(nèi)容就是順序有序加載粉寞。那么這個(gè)過(guò)程就是為了針對(duì)mach-o在加載到內(nèi)存中不是固定的首地址(ASLR)這一現(xiàn)象做數(shù)據(jù)修正的過(guò)程;由于ASLR(address space layout randomization)的存在左腔,可執(zhí)行文件和動(dòng)態(tài)鏈接庫(kù)在虛擬內(nèi)存中的加載地址每次啟動(dòng)都不固定唧垦,所以需要這2步來(lái)修復(fù)鏡像中的資源指針,來(lái)指向正確的地址液样。 rebase修復(fù)的是指向當(dāng)前鏡像內(nèi)部的資源指針振亮; 而bind指向的是鏡像外部的資源指針。
-
Bind image
將指針指向鏡像外部的內(nèi)容鞭莽,binding就是將這個(gè)二進(jìn)制調(diào)用的外部符號(hào)進(jìn)行綁定的過(guò)程坊秸。比如我們objc代碼中需要使用到NSObject, 即符號(hào)OBJC_CLASS$_NSObject,但是這個(gè)符號(hào)又不在我們的二進(jìn)制中澎怒,在系統(tǒng)庫(kù) Foundation.framework 中褒搔,因此就需要binding這個(gè)操作將對(duì)應(yīng)關(guān)系綁定到一起;Binding 看起來(lái)計(jì)算量比 Rebasing 更大喷面,但其實(shí)需要的 I/O 操作很少星瘾,Binding的時(shí)間主要是耗費(fèi)在計(jì)算上,因?yàn)镮O操作之前 Rebasing 已經(jīng)替 Binding 做過(guò)了乖酬,所以這兩個(gè)步驟的耗時(shí)是混在一起的死相。
可以從查看 __DATA 段中需要修正(fix-up)的指針,所以減少指針數(shù)量才會(huì)減少這部分工作的耗時(shí)咬像。對(duì)于 ObjC 來(lái)說(shuō)就是減少 Class,selector 和 category 這些元數(shù)據(jù)的數(shù)量算撮。從編碼原則和設(shè)計(jì)模式之類(lèi)的理論都會(huì)鼓勵(lì)大家多寫(xiě)精致短小的類(lèi)和方法,并將每部分方法獨(dú)立出一個(gè)類(lèi)別县昂,其實(shí)這會(huì)增加啟動(dòng)時(shí)間肮柜。
Objective-C 中有很多數(shù)據(jù)結(jié)構(gòu)都是靠 Rebasing 和 Binding 來(lái)修正(fix-up)的,比如 Class 中指向父類(lèi)的指針和指向方法的指針倒彰。Rebase&&Binding該階段的優(yōu)化關(guān)鍵在于減少__DATA segment中的指針數(shù)量。
-
Objc setup
在這一階段待讳,dyld會(huì)讀取二進(jìn)制文件的DATA段內(nèi)容芒澜,找到與objc相關(guān)的信息;注冊(cè) Objc 類(lèi)创淡,ObjC Runtime 需要維護(hù)一張映射類(lèi)名與類(lèi)的全局表痴晦。當(dāng)加載一個(gè) dylib 時(shí),其定義的所有的類(lèi)都需要被注冊(cè)到這個(gè)全局表中琳彩;讀取 protocol 以及 category 的信息誊酌,把category的定義插入方法列表 (category registration)部凑,確保 selector 的唯一性。
對(duì)于這一階段碧浊,我們能做的基本沒(méi)有涂邀,完全依賴上一步的優(yōu)化而減少耗時(shí)。
-
initializers
虛擬內(nèi)存動(dòng)態(tài)庫(kù)后邊存放的是堆區(qū)和棧區(qū)箱锐。這一階段的工作就是在這兩個(gè)區(qū)域展開(kāi)寫(xiě)入比勉。具體內(nèi)容是dyld開(kāi)始將程序二進(jìn)制文件初始化;交由ImageLoader讀取image瑞躺,其中包含了我們的類(lèi)敷搪、方法等各種符號(hào),由于runtime向dyld綁定了回調(diào)幢哨,當(dāng)image加載到內(nèi)存后赡勘,dyld會(huì)通知runtime進(jìn)行處理;runtime接手后調(diào)用mapimages做解析和處理捞镰,接下來(lái)loadimages中調(diào)用 callloadmethods方法闸与,遍歷所有加載進(jìn)來(lái)的Class,按繼承層級(jí)依次調(diào)用Class的+load方法和其 Category的+load方法岸售。執(zhí)行完后便會(huì)進(jìn)入main()階段践樱。
針對(duì)這一階段的優(yōu)化方案也是比較明顯。就是針對(duì)+load進(jìn)行處理凸丸,網(wǎng)上比較統(tǒng)一的答案是使用+initialize來(lái)替代+load的使用拷邢。這個(gè)方案的使用當(dāng)然是可以的,但是要注意邏輯的調(diào)用時(shí)機(jī)是否復(fù)合你的研發(fā)需求屎慢,還有就是需要注意做一些代碼執(zhí)行次數(shù)的保護(hù)瞭稼。
main階段
main() 函數(shù)執(zhí)行的階段,指的是從 main() 函數(shù)執(zhí)行開(kāi)始腻惠,到 appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相關(guān)方法執(zhí)行完成壹堰。
執(zhí)行到這一階段說(shuō)明dyld已經(jīng)進(jìn)入了程序的main()函數(shù)入口校套,UIApplicationMain回調(diào)代理方法didFinishLaunchingWithOptions狮斗。這里沒(méi)有需要優(yōu)化的地方亭罪,我們要做的就是處理didFinishLaunchingWithOptions方法執(zhí)行開(kāi)始到結(jié)束的這段時(shí)間的工作。
開(kāi)發(fā)者會(huì)把各種初始化工作都放到這個(gè)階段執(zhí)行欣喧,導(dǎo)致渲染完成滯后腌零。更加優(yōu)化的開(kāi)發(fā)方式,應(yīng)該是從功能上梳理出哪些是首屏渲染必要的初始化功能唆阿,哪些是 App 啟動(dòng)必要的初始化功能莱没,而哪些是只需要在對(duì)應(yīng)功能開(kāi)始使用時(shí)才需要初始化的。梳理完之后酷鸦,將這些初始化功能分別放到合適的階段進(jìn)行。
二、優(yōu)化方案
針對(duì)pre-main和main兩個(gè)階段臼隔,我們可以總結(jié)出以下的方法:
- pre-main階段
- 減少自定義動(dòng)態(tài)庫(kù)的依賴嘹裂;
- 合并多個(gè)自定義動(dòng)態(tài)庫(kù)為一個(gè)動(dòng)態(tài)庫(kù);
- 減少Objc類(lèi)的數(shù)量摔握,減少selector數(shù)量寄狼,刪除無(wú)用類(lèi)和函數(shù)(包括分類(lèi)),如果有必要可以嘗試合并一些類(lèi)氨淌;
- 減少一些無(wú)用的靜態(tài)變量泊愧;
- 減少C++虛函數(shù)的數(shù)量;
- 合理的+initializers替代+load的使用盛正;
- 盡量不要用到C++的靜態(tài)對(duì)象删咱;
- 類(lèi)名和方法名不宜過(guò)長(zhǎng),iOS每個(gè)類(lèi)和方法名都在__cstring段里都存了相應(yīng)的字符串值豪筝,所以類(lèi)和方法名的長(zhǎng)短也是對(duì)可執(zhí)行文件大小是有影響的痰滋。
- main階段
- 優(yōu)化代碼邏輯,去除一些非必要的邏輯和代碼续崖,減少每個(gè)流程所消耗的時(shí)間敲街;
- 減少啟動(dòng)初始化流程,酌情將一些初始化工作延后严望;
- 使用多線程來(lái)處理初始化工作多艇;
- 使用純代碼來(lái)構(gòu)建tabbar或nav等視圖,減少因?yàn)閤ib和storyboard解析成代碼帶來(lái)的消耗像吻。
三峻黍、實(shí)踐應(yīng)用
以下測(cè)試數(shù)據(jù)均來(lái)自我司線下版,不代表線上產(chǎn)品性能萧豆。
pre-main階段
main()之前的加載時(shí)間如何衡量奸披,蘋(píng)果提供了一種檢測(cè)pre-main階段的方法:DYLD_PRINT_STATISTICS = 1。
為了驗(yàn)證我們上文所說(shuō)的信息涮雷,我們分別做了兩次對(duì)比阵面,
//冷啟動(dòng)-1
Total pre-main time: 3.3 seconds (100.0%)
dylib loading time: 273.38 milliseconds (8.1%)
rebase/binding time: 163.78 milliseconds (4.8%)
ObjC setup time: 363.07 milliseconds (10.8%)
initializer time: 2.5 seconds (76.0%)
slowest intializers :
libSystem.B.dylib : 19.49 milliseconds (0.5%)
libMainThreadChecker.dylib : 92.20 milliseconds (2.7%)
libglInterpose.dylib : 164.11 milliseconds (4.9%)
libMTLInterpose.dylib : 69.42 milliseconds (2.0%)
美術(shù)寶1對(duì)1線下版 : 2.4 seconds (72.1%)
//冷啟動(dòng)-2
Total pre-main time: 3.3 seconds (100.0%)
dylib loading time: 249.00 milliseconds (7.4%)
rebase/binding time: 167.79 milliseconds (4.9%)
ObjC setup time: 404.63 milliseconds (12.0%)
initializer time: 2.5 seconds (75.5%)
slowest intializers :
libSystem.B.dylib : 16.98 milliseconds (0.5%)
libMainThreadChecker.dylib : 88.08 milliseconds (2.6%)
libglInterpose.dylib : 173.89 milliseconds (5.1%)
libMTLInterpose.dylib : 70.84 milliseconds (2.1%)
美術(shù)寶1對(duì)1線下版 : 2.4 seconds (72.2%)
//熱啟動(dòng)-1
Total pre-main time: 1.8 seconds (100.0%)
dylib loading time: 89.18 milliseconds (4.9%)
rebase/binding time: 168.99 milliseconds (9.3%)
ObjC setup time: 131.04 milliseconds (7.2%)
initializer time: 1.4 seconds (78.4%)
slowest intializers :
libSystem.B.dylib : 19.17 milliseconds (1.0%)
libMainThreadChecker.dylib : 58.62 milliseconds (3.2%)
libglInterpose.dylib : 161.28 milliseconds (8.9%)
libMTLInterpose.dylib : 42.63 milliseconds (2.3%)
ZegoLiveRoom : 39.11 milliseconds (2.1%)
美術(shù)寶1對(duì)1線下版 : 1.2 seconds (70.3%)
//熱啟動(dòng)-2
Total pre-main time: 1.3 seconds (100.0%)
dylib loading time: 172.99 milliseconds (12.9%)
rebase/binding time: 40.79 milliseconds (3.0%)
ObjC setup time: 63.74 milliseconds (4.7%)
initializer time: 1.0 seconds (79.2%)
slowest intializers :
libSystem.B.dylib : 11.00 milliseconds (0.8%)
libMainThreadChecker.dylib : 41.56 milliseconds (3.1%)
libglInterpose.dylib : 99.05 milliseconds (7.3%)
libMTLInterpose.dylib : 49.45 milliseconds (3.6%)
美術(shù)寶1對(duì)1線下版 : 1.0 seconds (74.9%)
因?yàn)榭紤]到dyld緩存的原因,所以針對(duì)冷啟動(dòng)的測(cè)試洪鸭,我會(huì)重啟設(shè)備后進(jìn)行打印样刷,熱啟動(dòng)的測(cè)試是在冷啟動(dòng)后我們殺掉應(yīng)用后間隔幾秒鐘再次開(kāi)啟應(yīng)用。測(cè)試結(jié)果也是比較明顯和直觀览爵,我司APP冷啟動(dòng)時(shí)的pre-main階段大約耗時(shí)3.3秒置鼻。這是個(gè)什么概念?老牛拉車(chē)蜓竹。
以下是WWDC2016 Apple給出的建議
Apple suggest to aim for a total app launch time of under 400ms and you must do it in less than 20 seconds or the system will kill your app.
Apple建議應(yīng)用的啟動(dòng)時(shí)間控制在400ms之下箕母。并且必須在20s以內(nèi)完成啟動(dòng)储藐,否則系統(tǒng)則會(huì)kill掉應(yīng)用程序。
從范圍400ms到20s可以看出嘶是,我們優(yōu)化的空間還蠻大钙勃,因?yàn)楫吘褂袀€(gè)底線400ms在嘛。分析一下數(shù)據(jù)聂喇,可以看出前四個(gè)階段的占比不高辖源,24%大約就是790ms左右,占比最大的其實(shí)是第五個(gè)階段(intializers階段)希太。
先從前四個(gè)階段入手克饶,看一下目前APP啟動(dòng)時(shí)大約用到了多少動(dòng)態(tài)庫(kù),
大約485個(gè)誊辉,其中有一些動(dòng)態(tài)庫(kù)是相同的地址矾湃,這個(gè)工程中的大部分哭都是來(lái)自des系統(tǒng)庫(kù),其中有兩個(gè)目錄下的文件是來(lái)自于我們自己的Mach-O芥映,DoraemonLoadAnalyze和KSYMediaPlayer洲尊,第一個(gè)是滴滴的性能檢測(cè)庫(kù),只有在線下版這種target下會(huì)進(jìn)行打包奈偏,KSYMediaPlayer是用于視頻播放的一個(gè)庫(kù)坞嘀,線上版和線下版都有存在,這個(gè)庫(kù)是沒(méi)有辦法去掉惊来。那么我們?cè)谕瑯拥木€下版環(huán)境下進(jìn)行以下對(duì)比測(cè)試丽涩,去掉第一個(gè)動(dòng)態(tài)庫(kù)試一試。得到的結(jié)果不是很理想裁蚁,這也在情理之中矢渊,結(jié)果還是3.3s左右。這個(gè)也是在情理之中的事情枉证,四百多個(gè)庫(kù)不會(huì)因?yàn)?兩個(gè)庫(kù)的額減少而出現(xiàn)明顯變化矮男。
接下來(lái)來(lái)處理下第五個(gè)階段,lldb幫我們打印出了最慢的幾個(gè)因素室谚。
- libSystem.B.dylib
- libMainThreadChecker.dylib
- libglInterpose.dylib
- libMTLInterpose.dylib
- 美術(shù)寶1對(duì)1線下版
根據(jù)之前的學(xué)習(xí)毡鉴,這個(gè)階段的大部分內(nèi)容是runtime在幫你處理初始化相關(guān)的工作。前四個(gè)因素我們沒(méi)有優(yōu)化的點(diǎn)秒赤,接著從美術(shù)寶1對(duì)1線下版這個(gè)Mach-O文件處理猪瞬,對(duì)其中所有重寫(xiě)了+load方法的地方我們重新處理看看效果。
主程序中重寫(xiě)了+load的類(lèi)大約有21個(gè)文件左右入篮,將這些工作延后處理陈瘦,得到以下測(cè)試數(shù)據(jù):
//冷啟動(dòng)
Total pre-main time: 2.9 seconds (100.0%)
dylib loading time: 407.79 milliseconds (13.7%)
rebase/binding time: 142.19 milliseconds (4.7%)
ObjC setup time: 370.54 milliseconds (12.4%)
initializer time: 2.0 seconds (68.9%)
slowest intializers :
libSystem.B.dylib : 20.20 milliseconds (0.6%)
libMainThreadChecker.dylib : 85.84 milliseconds (2.8%)
libglInterpose.dylib : 153.29 milliseconds (5.1%)
libMTLInterpose.dylib : 73.55 milliseconds (2.4%)
美術(shù)寶1對(duì)1線下版 : 1.9 seconds (65.1%)
//熱啟動(dòng)
Total pre-main time: 1.2 seconds (100.0%)
dylib loading time: 174.79 milliseconds (13.4%)
rebase/binding time: 38.48 milliseconds (2.9%)
ObjC setup time: 55.36 milliseconds (4.2%)
initializer time: 1.0 seconds (79.2%)
slowest intializers :
libSystem.B.dylib : 10.56 milliseconds (0.8%)
libMainThreadChecker.dylib : 41.27 milliseconds (3.1%)
libglInterpose.dylib : 111.59 milliseconds (8.6%)
libMTLInterpose.dylib : 32.40 milliseconds (2.5%)
美術(shù)寶1對(duì)1線下版 : 966.81 milliseconds (74.6%)
由于熱啟動(dòng)的數(shù)據(jù)根據(jù)緩存的因素耗時(shí)上下浮動(dòng),但是冷啟動(dòng)的數(shù)據(jù)比較穩(wěn)定潮售,通過(guò)對(duì)比可以看出我們的優(yōu)化還是很有效果的痊项,從整體啟動(dòng)時(shí)間3.3s下降到了2.9s锅风,其中主程MachO的耗時(shí)從2.4下降到了1.9s。到目前這里线婚,我們還可以繼續(xù)根據(jù)上面的方案繼續(xù)優(yōu)化pre-main這個(gè)階段的內(nèi)容遏弱,但是由于美術(shù)寶的項(xiàng)目過(guò)于龐大,加之組件化的加入塞弊,這里靠一人之力就不去嘗試測(cè)試了。筆者猜測(cè)這部分雖然還有優(yōu)化的空間泪姨,但是鑒于優(yōu)化方案的操作是減少類(lèi)名稱和方法名稱長(zhǎng)度游沿,從理論上來(lái)判斷應(yīng)該是通過(guò)減小體積來(lái)縮小IO時(shí)間,所以能繼續(xù)縮短多少時(shí)間肮砾,筆者猜也未必會(huì)太多诀黍,但是從數(shù)據(jù)來(lái)看耗時(shí)較大的地方是主包,所以從C++虛函數(shù)和構(gòu)造函數(shù)仗处、靜態(tài)變量眯勾、刪除無(wú)用類(lèi)來(lái)看的話,應(yīng)該還是有很可觀的優(yōu)化空間的婆誓。
main階段
這個(gè)階段我們主要從didFinishLaunchingWithOptions方法中進(jìn)行優(yōu)化吃环,首屏VC的初始化我們暫不考慮。代碼打點(diǎn)工具有很多洋幻,我們這里使用BLStopwatch郁轻,使用方法比較簡(jiǎn)單,記錄一個(gè)起始時(shí)間文留,再記錄一個(gè)截止時(shí)間好唯。我們先看一下優(yōu)化前的數(shù)據(jù):
//冷啟動(dòng)
15:45:33.171686+0800 美術(shù)寶1對(duì)1線下版[227:4773] 初始化耗時(shí):3.158380s
//熱啟動(dòng)
15:49:14.153892+0800 美術(shù)寶1對(duì)1線下版[237:6115] 初始化耗時(shí):2.000703s
如果你是用戶,新下載了APP燥翅,打開(kāi)耗時(shí)3.3+3.1s,你會(huì)怎么想骑篙?
可能是我自己的手機(jī)該換了。
讓我們來(lái)分析一下這個(gè)didFinishLaunchingWithOptions中到底做了什么事情森书,哪些事情是可以放到子線程中執(zhí)行靶端。
#1 神策完成: 0.753s
#2 防止崩潰完成: 1.112s
#3 bugly完成: 0.106s
#4 日志服務(wù)器hook完成: 0.020s
#5 httpDNS完成: 0.020s
#6 startLaunchInit完成: 0.649s
#7 開(kāi)啟APP額外初始化完成: 0.486s
#8 JPush初始化完成: 0.024s
#9 引導(dǎo)完成: 0.004s
首先看一下神策SDK的初始化,這個(gè)耗時(shí)相對(duì)真?zhèn)€階段的整體耗時(shí)來(lái)說(shuō)占了四分之一左右拄氯,筆者細(xì)細(xì)看了下神策開(kāi)發(fā)文檔躲查,其中提示要求在主線程初始化,并且咨詢了需求點(diǎn)是用來(lái)埋點(diǎn)統(tǒng)計(jì)使用译柏,所以無(wú)法將其放到子線程或延時(shí)初始化镣煮,目前也只能放到這里暫不修改。
防崩潰初始化工作鄙麦,這個(gè)地方耗時(shí)1.112s典唇,其中主要工作是進(jìn)行一些類(lèi)的方法交換镊折,時(shí)間全浪費(fèi)在這個(gè)主線程處理一些與UI無(wú)關(guān)的工作上了,我么將其滯后處理介衔,放到+initialize中處理恨胚。
關(guān)于bugly這部分的初始化工作,不知道會(huì)不會(huì)和神策的需求有冗余炎咖,如果有的話可以嘗試去掉一個(gè)赃泡,bugly目前的耗時(shí)不是很多0.106s左右,看了bugly的文檔沒(méi)有提示說(shuō)要在主線程乘盼,但是對(duì)于這種統(tǒng)計(jì)性的SDK升熊,筆者認(rèn)為還是放到這里不要?jiǎng)恿耍讓訖C(jī)制透明的绸栅,我們無(wú)法預(yù)料放到其他線程或延時(shí)初始化有沒(méi)有影響级野。
日志服務(wù)器hook初始化和httpDNS的工作這里耗時(shí)0.02s+0.02s,蒼蠅雖小仍是肉粹胯,也不能放過(guò)優(yōu)化蓖柔,看了內(nèi)部實(shí)現(xiàn),果斷放到了子線程中處理风纠。
startLaunchInit和引導(dǎo)和UI相關(guān)况鸣、極光推送的內(nèi)容放主線程中暫不處理。
以下是修改后的數(shù)據(jù):
#1 神策完成: 0.756
#2 防崩潰完成: 0.001
#3 bugly完成: 0.132
#4 日志服務(wù)器hook完成: 0.000
#5 httpDNS完成: 0.000
#6 startLaunchInit完成: 0.627
#7 初始化啟動(dòng)頁(yè)面完成: 0.408
#8 JPush完成: 0.042
#9 引導(dǎo)獲取完成: 0.021
//冷啟動(dòng)
16:45:33.171686+0800 美術(shù)寶1對(duì)1線下版[227:4773] 初始化耗時(shí):1.958380s
到目前為止項(xiàng)目?jī)?yōu)化啟動(dòng)時(shí)間大約1.5s左右议忽。啟動(dòng)優(yōu)化除了受技術(shù)本身影響還和你的業(yè)務(wù)需求息息相關(guān)懒闷。這部分工作我們還有很大的提升空間,文章中如有問(wèn)題歡迎校正栈幸。