感謝分享:http://www.zoomfeng.com/blog/launch-time.html
背景
一個(gè)項(xiàng)目做的時(shí)間長(zhǎng)了唆涝,啟動(dòng)流程往往容易雜亂涂乌,庫(kù)也用的越來(lái)越多九榔,APP的啟動(dòng)時(shí)間也會(huì)慢慢變長(zhǎng)滨达。本次將針對(duì)iOS APP的啟動(dòng)時(shí)間優(yōu)化一波巷燥。
通常針對(duì)一個(gè)技術(shù)點(diǎn)做優(yōu)化的時(shí)候,都要先了解清楚這個(gè)技術(shù)點(diǎn)有哪些流程,優(yōu)化的方向往往是減少流程的數(shù)量权悟,以及減少每個(gè)流程的消耗。
本次優(yōu)化從結(jié)果上來(lái)看推盛,main階段的優(yōu)化效果最顯著峦阁,尤其是啟動(dòng)時(shí)的一些IO操作處理,對(duì)啟動(dòng)時(shí)間的減少有很大作用耘成。多線程啟動(dòng)的設(shè)計(jì)和驗(yàn)證最有意思榔昔,但是在實(shí)踐上由于我們業(yè)務(wù)本身的原因,只開(kāi)了額外一個(gè)子線程來(lái)并行啟動(dòng)瘪菌,且僅在子線程做了少量的獨(dú)立操作撒会,主要還是我們的業(yè)務(wù)之間耦合性太強(qiáng)了,不太適合拆分到子線程师妙。
一般說(shuō)來(lái)诵肛,pre-main階段的定義為APP開(kāi)始啟動(dòng)到系統(tǒng)調(diào)用main函數(shù)這一段時(shí)間;main階段則代表從main函數(shù)入口到主UI框架的viewDidAppear函數(shù)調(diào)用的這一段時(shí)間默穴。(本文后續(xù)main階段的時(shí)間統(tǒng)計(jì)都用viewDidAppear
作為基準(zhǔn)而非的applicationWillFinishLaunching
)
本文前半部分講原理(內(nèi)容基本是從網(wǎng)上借鑒/摘錄)怔檩,后半部分講實(shí)踐,pre-main階段的原理比較難理解蓄诽,不過(guò)實(shí)踐倒是根據(jù)結(jié)論直接做就好了薛训。
App啟動(dòng)過(guò)程
①解析Info.plist
加載相關(guān)信息,例如閃屏
沙箱建立仑氛、權(quán)限檢查
②Mach-O加載
如果是胖二進(jìn)制文件乙埃,尋找合適當(dāng)前CPU架構(gòu)的部分
加載所有依賴的Mach-O文件(遞歸調(diào)用Mach-O加載的方法)
定位內(nèi)部、外部指針引用锯岖,例如字符串介袜、函數(shù)等
執(zhí)行聲明為_(kāi)_attribute__((constructor))的C函數(shù)
加載類擴(kuò)展(Category)中的方法
C++靜態(tài)對(duì)象加載、調(diào)用ObjC的 +load 函數(shù)
③程序執(zhí)行
調(diào)用main()
調(diào)用UIApplicationMain()
調(diào)用applicationWillFinishLaunching
換成另一個(gè)說(shuō)法就是:
App開(kāi)始啟動(dòng)后嚎莉,系統(tǒng)首先加載可執(zhí)行文件(自身App的所有.o文件的集合)米酬,然后加載動(dòng)態(tài)鏈接器dyld,dyld是一個(gè)專門用來(lái)加載動(dòng)態(tài)鏈接庫(kù)的庫(kù)趋箩。 執(zhí)行從dyld開(kāi)始赃额,dyld從可執(zhí)行文件的依賴開(kāi)始, 遞歸加載所有的依賴動(dòng)態(tài)鏈接庫(kù)。
動(dòng)態(tài)鏈接庫(kù)包括:iOS 中用到的所有系統(tǒng) framework叫确,加載OC runtime方法的libobjc跳芳,系統(tǒng)級(jí)別的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)竹勉。
可執(zhí)行文件的內(nèi)核流程
如圖飞盆,當(dāng)啟動(dòng)一個(gè)應(yīng)用程序時(shí),系統(tǒng)最后會(huì)根據(jù)你的行為調(diào)用兩個(gè)函數(shù),fork和execve吓歇。fork功能創(chuàng)建一個(gè)進(jìn)程孽水;execve功能加載和運(yùn)行程序。這里有多個(gè)不同的功能城看,比如execl,execv和exect女气,每個(gè)功能提供了不同傳參和環(huán)境變量的方法到程序中。在OSX中测柠,每個(gè)這些其他的exec路徑最終調(diào)用了內(nèi)核路徑execve炼鞠。
1、執(zhí)行exec系統(tǒng)調(diào)用轰胁,一般都是這樣谒主,用fork()函數(shù)新建立一個(gè)進(jìn)程,然后讓進(jìn)程去執(zhí)行exec調(diào)用赃阀。我們知道霎肯,在fork()建立新進(jìn)程之后,父進(jìn)程與子進(jìn)程共享代碼段(TEXT)凹耙,但數(shù)據(jù)空間(DATA)是分開(kāi)的姿现,但父進(jìn)程會(huì)把自己數(shù)據(jù)空間的內(nèi)容copy到子進(jìn)程中去,還有上下文也會(huì)copy到子進(jìn)程中去肖抱。
2备典、為了提高效率,采用一種寫時(shí)copy的策略意述,即創(chuàng)建子進(jìn)程的時(shí)候提佣,并不copy父進(jìn)程的地址空間,父子進(jìn)程擁有共同的地址空間荤崇,只有當(dāng)子進(jìn)程需要寫入數(shù)據(jù)時(shí)(如向緩沖區(qū)寫入數(shù)據(jù))拌屏,這時(shí)候會(huì)復(fù)制地址空間,復(fù)制緩沖區(qū)到子進(jìn)程中去术荤。從而父子進(jìn)程擁有獨(dú)立的地址空間倚喂。而對(duì)于fork()之后執(zhí)行exec之前,這種策略能夠很好的提高效率瓣戚,如果一開(kāi)始就copy端圈,那么exec之后,子進(jìn)程(此處可以說(shuō)是父進(jìn)程也可以說(shuō)是子進(jìn)程子库,因?yàn)閭z進(jìn)程的數(shù)據(jù)此時(shí)是一樣的)的數(shù)據(jù)會(huì)被放棄舱权,被新的進(jìn)程所代替。見(jiàn)下圖:
啟動(dòng)時(shí)間的分布仑嗅,pre-main和main階段原理淺析
rebase修復(fù)的是指向當(dāng)前鏡像內(nèi)部的資源指針宴倍; 而bind指向的是鏡像外部的資源指針张症。
rebase步驟先進(jìn)行,需要把鏡像讀入內(nèi)存鸵贬,并以page為單位進(jìn)行加密驗(yàn)證俗他,保證不會(huì)被篡改,所以這一步的瓶頸在IO阔逼。bind在其后進(jìn)行拯辙,由于要查詢符號(hào)表,來(lái)指向跨鏡像的資源颜价,加上在rebase階段,鏡像已被讀入和加密驗(yàn)證诉濒,所以這一步的瓶頸在于CPU計(jì)算周伦。這兩個(gè)步驟在下面會(huì)詳細(xì)闡述。
pre-main過(guò)程
main過(guò)程
一些概念
什么是dyld?
動(dòng)態(tài)鏈接庫(kù)的加載過(guò)程主要由dyld來(lái)完成未荒,dyld是蘋果的動(dòng)態(tài)鏈接器专挪。
系統(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)目中所有類進(jìn)行類結(jié)構(gòu)初始化,然后調(diào)用所有的load方法庙楚。最后dyld返回main函數(shù)地址上荡,main函數(shù)被調(diào)用,我們便來(lái)到了熟悉的程序入口馒闷。
當(dāng)加載一個(gè) Mach-O 文件 (一個(gè)可執(zhí)行文件或者一個(gè)庫(kù)) 時(shí)酪捡,動(dòng)態(tài)鏈接器首先會(huì)檢查共享緩存看看是否存在其中,如果存在窜司,那么就直接從共享緩存中拿出來(lái)使用沛善。每一個(gè)進(jìn)程都把這個(gè)共享緩存映射到了自己的地址空間中。這個(gè)方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動(dòng)時(shí)間塞祈。
問(wèn)題:測(cè)試發(fā)現(xiàn)金刁,由于手機(jī)從開(kāi)機(jī)后帅涂,連續(xù)兩次啟動(dòng)同一個(gè)APP的pre-main實(shí)際時(shí)間的差值比較大,這一步可以在真機(jī)上復(fù)現(xiàn)尤蛮,那么這兩次啟動(dòng)pre-main的時(shí)間差值媳友,是跟系統(tǒng)的framework關(guān)系比較大,還是跟APP自身依賴的第三方framework關(guān)系比較大呢产捞?
回答:操作系統(tǒng)對(duì)于動(dòng)態(tài)庫(kù)有一個(gè)共享的空間醇锚,在這個(gè)空間被填滿,或者沒(méi)有其他機(jī)制來(lái)清理這一塊的內(nèi)存之前坯临,動(dòng)態(tài)庫(kù)被加載到內(nèi)存后就一直存在焊唬。所以,問(wèn)題中開(kāi)機(jī)后連續(xù)啟動(dòng)同一個(gè)APP兩次的pre-main時(shí)間的差值看靠,可以認(rèn)為是動(dòng)態(tài)庫(kù)被第一次加載后緩存到內(nèi)存造成的赶促,時(shí)間上也肯定是第二次比第一次快。比如有一些系統(tǒng)的動(dòng)態(tài)庫(kù)挟炬,操作系統(tǒng)還暫時(shí)沒(méi)用到鸥滨,但是你的APP用到了,在第一次啟動(dòng)APP就會(huì)加載到內(nèi)存谤祖,第二次就直接拿內(nèi)存里的婿滓。你自己APP用到的動(dòng)態(tài)庫(kù)也類似,只不過(guò)APP自己的動(dòng)態(tài)庫(kù)只能共享給自己的Extension粥喜,而不能給別的進(jìn)程凸主,進(jìn)程有相互獨(dú)立的地址空間,而且你的APP是用戶態(tài)容客,而不是內(nèi)核態(tài)秕铛,不能像系統(tǒng)的動(dòng)態(tài)庫(kù)那樣被所有進(jìn)程訪問(wèn)。詳情見(jiàn)《現(xiàn)代操作系統(tǒng)》缩挑。
Mach-O 鏡像文件
Mach-O 被劃分成一些 segement但两,每個(gè) segement 又被劃分成一些 section。segment 的名字都是大寫的供置,且空間大小為頁(yè)的整數(shù)谨湘。頁(yè)的大小跟硬件有關(guān),在 arm64 架構(gòu)一頁(yè)是 16KB芥丧,其余為 4KB紧阔。
section 雖然沒(méi)有整數(shù)倍頁(yè)大小的限制,但是 section 之間不會(huì)有重疊续担。幾乎所有 Mach-O 都包含這三個(gè)段(segment): __TEXT
擅耽,__DATA
和__LINKEDIT
。
__TEXT
包含 Mach header物遇,被執(zhí)行的代碼和只讀常量(如C 字符串)乖仇。只讀可執(zhí)行(r-x)憾儒。
__DATA
包含全局變量,靜態(tài)變量等乃沙∑鹬海可讀寫(rw-)。
__LINKEDIT
包含了加載程序的『元數(shù)據(jù)』警儒,比如函數(shù)的名稱和地址训裆。只讀(r–)。
ASLR(Address Space Layout Randomization):地址空間布局隨機(jī)化蜀铲,鏡像會(huì)在隨機(jī)的地址上加載边琉。
傳統(tǒng)方式下,進(jìn)程每次啟動(dòng)采用的都是固定可預(yù)見(jiàn)的方式记劝,這意味著一個(gè)給定的程序在給定的架構(gòu)上的進(jìn)程初始虛擬內(nèi)存都是基本一致的艺骂,而且在進(jìn)程正常運(yùn)行的生命周期中,內(nèi)存中的地址分布具有非常強(qiáng)的可預(yù)測(cè)性隆夯,這給了黑客很大的施展空間(代碼注入,重寫內(nèi)存)别伏;
如果采用ASLR蹄衷,進(jìn)程每次啟動(dòng),地址空間都會(huì)被簡(jiǎn)單地隨機(jī)化厘肮,但是只是偏移愧口,不是攪亂。大體布局——程序文本类茂、數(shù)據(jù)和庫(kù)是一樣的耍属,但是具體的地址都不同了,可以阻擋黑客對(duì)地址的猜測(cè) 巩检。
代碼簽名:可能我們認(rèn)為 Xcode 會(huì)把整個(gè)文件都做加密 hash 并用做數(shù)字簽名厚骗。其實(shí)為了在運(yùn)行時(shí)驗(yàn)證 Mach-O 文件的簽名,并不是每次重復(fù)讀入整個(gè)文件兢哭,而是把每頁(yè)內(nèi)容都生成一個(gè)單獨(dú)的加密散列值领舰,并存儲(chǔ)在 __LINKEDIT 中。這使得文件每頁(yè)的內(nèi)容都能及時(shí)被校驗(yàn)確并保不被篡改迟螺。
關(guān)于虛擬內(nèi)存
我們開(kāi)發(fā)者開(kāi)發(fā)過(guò)程中所接觸到的內(nèi)存均為虛擬內(nèi)存冲秽,虛擬內(nèi)存使App認(rèn)為它擁有連續(xù)的可用的內(nèi)存(一個(gè)連續(xù)完整的地址空間),這是系統(tǒng)給我們的饋贈(zèng)矩父,而實(shí)際上锉桑,它通常是分布在多個(gè)物理內(nèi)存碎片,系統(tǒng)的虛擬內(nèi)存空間映射vm_map負(fù)責(zé)虛擬內(nèi)存和物理內(nèi)存的映射關(guān)系窍株。
ARM處理器64bit的架構(gòu)情況下民轴,也就是0x000000000 - 0xFFFFFFFFF攻柠,每個(gè)16進(jìn)制數(shù)是4位,即2的36次冪杉武,就是64GB辙诞,即App最大的虛擬內(nèi)存空間為64GB。
共享動(dòng)態(tài)庫(kù)其實(shí)就是共享的物理內(nèi)存中的那份動(dòng)態(tài)庫(kù)轻抱,App虛擬內(nèi)存中的共享動(dòng)態(tài)庫(kù)并未真實(shí)分配物理內(nèi)存飞涂,使用時(shí)虛擬內(nèi)存會(huì)訪問(wèn)同一份物理內(nèi)存達(dá)到共享動(dòng)態(tài)庫(kù)的目的,iPhone7 PLUS(之前的產(chǎn)品最大為2GB)的物理內(nèi)存RAM也只有3GB祈搜,那么超過(guò)3GB的物理內(nèi)存如何處理呢较店,系統(tǒng)會(huì)使用一部分硬盤空間ROM來(lái)充當(dāng)內(nèi)存使用,在需要時(shí)進(jìn)行數(shù)據(jù)交換容燕,當(dāng)然磁盤的數(shù)據(jù)交換是遠(yuǎn)遠(yuǎn)慢于物理內(nèi)存的梁呈,這也是我們內(nèi)存過(guò)載時(shí),App卡頓的原因之一蘸秘。
系統(tǒng)使用動(dòng)態(tài)鏈接有幾點(diǎn)好處:
代碼共用:很多程序都動(dòng)態(tài)鏈接了這些 lib官卡,但它們?cè)趦?nèi)存和磁盤中中只有一份。
易于維護(hù):由于被依賴的 lib 是程序執(zhí)行時(shí)才鏈接的醋虏,所以這些 lib 很容易做更新寻咒,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升級(jí)直接換成libSystem.C.dylib 然后再替換替身就行了颈嚼。
減少可執(zhí)行文件體積:相比靜態(tài)鏈接毛秘,動(dòng)態(tài)鏈接在編譯時(shí)不需要打進(jìn)去,所以可執(zhí)行文件的體積要小很多阻课。
上圖中叫挟,TEXT段兩個(gè)進(jìn)程共用,DATA段每個(gè)進(jìn)程各一份限煞。
下面開(kāi)始詳細(xì)分析pre-main的各個(gè)階段
加載 Dylib
從主執(zhí)行文件的 header 獲取到需要加載的所依賴動(dòng)態(tài)庫(kù)列表抹恳,而 header 早就被內(nèi)核映射過(guò)。然后它需要找到每個(gè) dylib署驻,然后打開(kāi)文件讀取文件起始位置适秩,確保它是 Mach-O 文件。接著會(huì)找到代碼簽名并將其注冊(cè)到內(nèi)核硕舆。然后在 dylib 文件的每個(gè) segment 上調(diào)用 mmap()秽荞。應(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)凌节,加載速度很快钦听。
加載系統(tǒng)的 dylib 很快洒试,因?yàn)橛袃?yōu)化(因?yàn)椴僮飨到y(tǒng)自己要用部分framework所以在操作系統(tǒng)開(kāi)機(jī)后就已經(jīng)加載到內(nèi)存了)。但加載內(nèi)嵌(embedded)的 dylib 文件很占時(shí)間朴上,所以盡可能把多個(gè)內(nèi)嵌 dylib 合并成一個(gè)來(lái)加載垒棋,或者使用 static archive。使用 dlopen() 來(lái)在運(yùn)行時(shí)懶加載是不建議的痪宰,這么做可能會(huì)帶來(lái)一些問(wèn)題叼架,并且總的開(kāi)銷更大。
在每個(gè)動(dòng)態(tài)庫(kù)的加載過(guò)程中衣撬, dyld需要:
①分析所依賴的動(dòng)態(tài)庫(kù)
②找到動(dòng)態(tài)庫(kù)的mach-o文件
③打開(kāi)文件
④驗(yàn)證文件
⑤在系統(tǒng)核心注冊(cè)文件簽名
⑥對(duì)動(dòng)態(tài)庫(kù)的每一個(gè)segment調(diào)用mmap()
針對(duì)這一步驟的優(yōu)化有:
①減少非系統(tǒng)庫(kù)的依賴乖订;
②使用靜態(tài)庫(kù)而不是動(dòng)態(tài)庫(kù);
③合并非系統(tǒng)動(dòng)態(tài)庫(kù)為一個(gè)動(dòng)態(tài)庫(kù)具练;
Rebase && Binding
Fix-ups
在加載所有的動(dòng)態(tài)鏈接庫(kù)之后乍构,它們只是處在相互獨(dú)立的狀態(tài),需要將它們綁定起來(lái)扛点,這就是 Fix-ups哥遮。代碼簽名使得我們不能修改指令,那樣就不能讓一個(gè) dylib 的調(diào)用另一個(gè) dylib陵究。這時(shí)需要加很多間接層昔善。
現(xiàn)代 code-gen 被叫做動(dòng)態(tài) PIC(Position Independent Code),意味著代碼可以被加載到間接的地址上畔乙。當(dāng)調(diào)用發(fā)生時(shí),code-gen 實(shí)際上會(huì)在 __DATA 段中創(chuàng)建一個(gè)指向被調(diào)用者的指針翩概,然后加載指針并跳轉(zhuǎn)過(guò)去牲距。
所以 dyld 做的事情就是修正(fix-up)指針和數(shù)據(jù)。Fix-up 有兩種類型钥庇,rebasing 和 binding牍鞠。
Rebase
Rebasing:在鏡像內(nèi)部調(diào)整指針的指向,針對(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指向的是鏡像外部的資源指針嗦枢。
在iOS4.3前會(huì)把dylib加載到指定地址攀芯,所有指針和數(shù)據(jù)對(duì)于代碼來(lái)說(shuō)都是固定的,dyld 就無(wú)需做rebase/binding了文虏。
iOS4.3后引入了 ASLR 侣诺,dylib會(huì)被加載到隨機(jī)地址殖演,這個(gè)隨機(jī)的地址跟代碼和數(shù)據(jù)指向的舊地址會(huì)有偏差,dyld 需要修正這個(gè)偏差年鸳,做法就是將 dylib 內(nèi)部的指針地址都加上這個(gè)偏移量趴久,偏移量的計(jì)算方法如下:
Slide = actual_address - preferred_address
然后就是重復(fù)不斷地對(duì) __DATA 段中需要 rebase 的指針加上這個(gè)偏移量。這就又涉及到 page fault 和 COW搔确。這可能會(huì)產(chǎn)生 I/O 瓶頸彼棍,但因?yàn)?rebase 的順序是按地址排列的,所以從內(nèi)核的角度來(lái)看這是個(gè)有次序的任務(wù)妥箕,它會(huì)預(yù)先讀入數(shù)據(jù)滥酥,減少 I/O 消耗。
在 Rebasing 和 Binding 前會(huì)判斷是否已經(jīng) Prebinding畦幢。如果已經(jīng)進(jìn)行過(guò)預(yù)綁定(Prebinding)坎吻,那就不需要 Rebasing 和 Binding 這些 Fix-up 流程了,因?yàn)橐呀?jīng)在預(yù)先綁定的地址加載好了宇葱。
rebase步驟先進(jìn)行瘦真,需要把鏡像讀入內(nèi)存,并以page為單位進(jìn)行加密驗(yàn)證黍瞧,保證不會(huì)被篡改诸尽,所以這一步的瓶頸在IO。bind在其后進(jìn)行印颤,由于要查詢符號(hào)表您机,來(lái)指向跨鏡像的資源,加上在rebase階段年局,鏡像已被讀入和加密驗(yàn)證际看,所以這一步的瓶頸在于CPU計(jì)算。
Binding
Binding:將指針指向鏡像外部的內(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)系綁定到一起;
lazyBinding就是在加載動(dòng)態(tài)庫(kù)的時(shí)候不會(huì)立即binding, 當(dāng)時(shí)當(dāng)?shù)谝淮握{(diào)用這個(gè)方法的時(shí)候再實(shí)施binding验庙。 做到的方法也很簡(jiǎn)單: 通過(guò)dyld_stub_binder這個(gè)符號(hào)來(lái)做顶吮。lazyBinding的方法第一次會(huì)調(diào)用到dyld_stub_binder, 然后dyld_stub_binder負(fù)責(zé)找到真實(shí)的方法,并且將地址bind到樁上粪薛,下一次就不用再bind了云矫。
Binding 是處理那些指向 dylib 外部的指針,它們實(shí)際上被符號(hào)(symbol)名稱綁定,也就是個(gè)字符串让禀。__LINKEDIT
段中也存儲(chǔ)了需要 bind 的指針挑社,以及指針需要指向的符號(hào)。dyld 需要找到 symbol 對(duì)應(yīng)的實(shí)現(xiàn)巡揍,這需要很多計(jì)算痛阻,去符號(hào)表里查找。找到后會(huì)將內(nèi)容存儲(chǔ)到__DATA
段中的那個(gè)指針中腮敌。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ì)模式之類的理論都會(huì)鼓勵(lì)大家多寫精致短小的類和方法刨裆,并將每部分方法獨(dú)立出一個(gè)類別澈圈,其實(shí)這會(huì)增加啟動(dòng)時(shí)間。對(duì)于 C++ 來(lái)說(shuō)需要減少虛方法帆啃,因?yàn)樘摲椒〞?huì)創(chuàng)建 vtable瞬女,這也會(huì)在__DATA
段中創(chuàng)建結(jié)構(gòu)。雖然 C++ 虛方法對(duì)啟動(dòng)耗時(shí)的增加要比 ObjC 元數(shù)據(jù)要少努潘,但依然不可忽視诽偷。最后推薦使用 Swift 結(jié)構(gòu)體,它需要 fix-up 的內(nèi)容較少疯坤。
Objective-C 中有很多數(shù)據(jù)結(jié)構(gòu)都是靠 Rebasing 和 Binding 來(lái)修正(fix-up)的报慕,比如 Class 中指向父類的指針和指向方法的指針。
Rebase&&Binding該階段的優(yōu)化關(guān)鍵在于減少__DATA
segment中的指針數(shù)量贴膘。我們可以優(yōu)化的點(diǎn)有:
①減少Objc類數(shù)量, 減少selector數(shù)量略号,把未使用的類和函數(shù)都可以刪掉
②減少C++虛函數(shù)數(shù)量
③轉(zhuǎn)而使用swift stuct(其實(shí)本質(zhì)上就是為了減少符號(hào)的數(shù)量刑峡,使用swift語(yǔ)言來(lái)開(kāi)發(fā)?)
未使用類的掃描,可以利用linkmap文件和otool工機(jī)具反編譯APP的可進(jìn)行二進(jìn)制文件得出一個(gè)大概的結(jié)果玄柠,但是不算非常精確突梦,掃描出來(lái)后需要手動(dòng)一個(gè)個(gè)確認(rèn)。掃描原理大致是classlist和classref兩者的差值羽利,所有的類和使用了的類的差值就是未使用的類啦宫患。因?yàn)槲词褂玫念愔饕獌?yōu)化的是pre-main的時(shí)間,根據(jù)測(cè)試我們的工程pre-main時(shí)間并不長(zhǎng)这弧,所以本次并沒(méi)有針對(duì)這一塊做優(yōu)化娃闲。(TODO:寫腳本來(lái)驗(yàn)證這一點(diǎn))虚汛。
ObjC SetUp
主要做以下幾件事來(lái)完成Objc Setup:
①讀取二進(jìn)制文件的 DATA 段內(nèi)容,找到與 objc 相關(guān)的信息
②注冊(cè) Objc 類皇帮,ObjC Runtime 需要維護(hù)一張映射類名與類的全局表卷哩。當(dāng)加載一個(gè) dylib 時(shí),其定義的所有的類都需要被注冊(cè)到這個(gè)全局表中属拾;
③讀取 protocol 以及 category 的信息将谊,把category的定義插入方法列表 (category registration)察蹲,
④確保 selector 的唯一性
ObjC 是個(gè)動(dòng)態(tài)語(yǔ)言辨绊,可以用類的名字來(lái)實(shí)例化一個(gè)類的對(duì)象苇经。這意味著 ObjC Runtime 需要維護(hù)一張映射類名與類的全局表逸寓。當(dāng)加載一個(gè) dylib 時(shí)匈勋,其定義的所有的類都需要被注冊(cè)到這個(gè)全局表中崎弃。
C++ 中有個(gè)問(wèn)題叫做易碎的基類(fragile base class)纤虽。ObjC 就沒(méi)有這個(gè)問(wèn)題阅虫,因?yàn)闀?huì)在加載時(shí)通過(guò) fix-up 動(dòng)態(tài)類中改變實(shí)例變量的偏移量托酸。
在 ObjC 中可以通過(guò)定義類別(Category)的方式改變一個(gè)類的方法褒颈。有時(shí)你想要添加方法的類在另一個(gè) dylib 中,而不在你的鏡像中(也就是對(duì)系統(tǒng)或別人的類動(dòng)刀)励堡,這時(shí)也需要做些 fix-up谷丸。
ObjC 中的 selector 必須是唯一的。
由于之前2步驟的優(yōu)化应结,這一步實(shí)際上沒(méi)有什么可做的刨疼。幾乎都靠 Rebasing 和 Binding 步驟中減少所需 fix-up 內(nèi)容。因?yàn)榍懊娴墓ぷ饕矔?huì)使得這步耗時(shí)減少鹅龄。
Initializers
以上三步屬于靜態(tài)調(diào)整揩慕,都是在修改__DATA segment中的內(nèi)容,而這里則開(kāi)始動(dòng)態(tài)調(diào)整扮休,開(kāi)始在堆和棧中寫入內(nèi)容迎卤。 工作主要有:
1、Objc的+load()函數(shù)
2玷坠、C++的構(gòu)造函數(shù)屬性函數(shù) 形如attribute((constructor)) void DoSomeInitializationWork()
3蜗搔、非基本類型的C++靜態(tài)全局變量的創(chuàng)建(通常是類或結(jié)構(gòu)體)(non-trivial initializer) 比如一個(gè)全局靜態(tài)結(jié)構(gòu)體的構(gòu)建,如果在構(gòu)造函數(shù)中有繁重的工作八堡,那么會(huì)拖慢啟動(dòng)速度
Objc的load函數(shù)和C++的靜態(tài)構(gòu)造函數(shù)采用由底向上的方式執(zhí)行樟凄,來(lái)保證每個(gè)執(zhí)行的方法,都可以找到所依賴的動(dòng)態(tài)庫(kù)
1兄渺、dyld開(kāi)始將程序二進(jìn)制文件初始化
2缝龄、交由ImageLoader讀取image,其中包含了我們的類、方法等各種符號(hào)
3叔壤、由于runtime向dyld綁定了回調(diào)瞎饲,當(dāng)image加載到內(nèi)存后,dyld會(huì)通知runtime進(jìn)行處理
4百新、runtime接手后調(diào)用mapimages做解析和處理企软,接下來(lái)loadimages中調(diào)用 callloadmethods方法,遍歷所有加載進(jìn)來(lái)的Class饭望,按繼承層級(jí)依次調(diào)用Class的+load方法和其 Category的+load方法
整個(gè)事件由dyld主導(dǎo)仗哨,完成運(yùn)行環(huán)境的初始化后,配合ImageLoader 將二進(jìn)制文件按格式加載到內(nèi)存铅辞,動(dòng)態(tài)鏈接依賴庫(kù)厌漂,并由runtime負(fù)責(zé)加載成objc 定義的結(jié)構(gòu),所有初始化工作結(jié)束后斟珊,dyld調(diào)用真正的main函數(shù)
C++ 會(huì)為靜態(tài)創(chuàng)建的對(duì)象生成初始化器苇倡。而在 ObjC 中有個(gè)叫 +load 的方法,然而它被廢棄了囤踩,現(xiàn)在建議使用 +initialize旨椒。對(duì)比詳見(jiàn)StackOverflow的一個(gè)連接;
這一步可以做的優(yōu)化有:
①使用 +initialize 來(lái)替代 +load
②不要使用 atribute((constructor)) 將方法顯式標(biāo)記為初始化器,而是讓初始化方法調(diào)用時(shí)才執(zhí)行堵漱。
比如使用 dispatch_once(),pthread_once() 或 std::once()综慎。也就是在第一次使用時(shí)才初始化,推遲了一部分工作耗時(shí)勤庐。
也盡量不要用到C++的靜態(tài)對(duì)象示惊。
從效率上來(lái)說(shuō),在+load 和+initialize里執(zhí)行同樣的代碼愉镰,效率是一樣的米罚,即使有差距,也不會(huì)差距太大丈探。 但所有的+load 方法都在啟動(dòng)的時(shí)候調(diào)用录择,方法多了就會(huì)嚴(yán)重影響啟動(dòng)速度了。 就說(shuō)我們項(xiàng)目中碗降,有200個(gè)左右+load方法隘竭,一共耗時(shí)大概1s 左右,這塊就會(huì)嚴(yán)重影響到用戶感知了遗锣。 而+initialize方法是在對(duì)應(yīng) Class 第一次使用的時(shí)候調(diào)用货裹,這是一個(gè)懶加載的方法嗤形,理想情況下精偿,這200個(gè)+load方法都使用+initialize來(lái)代替,將耗時(shí)分?jǐn)偟接脩羰褂眠^(guò)程中,每個(gè)方法平均耗時(shí)只有5ms笔咽,用戶完全可以無(wú)感知搔预。 因?yàn)閘oad是在啟動(dòng)的時(shí)候調(diào)用,而initialize是在類首次被使用的時(shí)候調(diào)用叶组,不過(guò)當(dāng)你把load中的邏輯移到initialize中時(shí)候拯田,一定要注意initialize的重復(fù)調(diào)用問(wèn)題,能用dispatch_once()來(lái)完成的甩十,就盡量不要用到load方法船庇。
如果程序剛剛被運(yùn)行過(guò),那么程序的代碼會(huì)被dyld緩存侣监,因此即使殺掉進(jìn)程再次重啟加載時(shí)間也會(huì)相對(duì)快一點(diǎn)鸭轮,如果長(zhǎng)時(shí)間沒(méi)有啟動(dòng)或者當(dāng)前dyld的緩存已經(jīng)被其他應(yīng)用占據(jù),那么這次啟動(dòng)所花費(fèi)的時(shí)間就要長(zhǎng)一點(diǎn)橄霉,這就分別是熱啟動(dòng)和冷啟動(dòng)的概念窃爷。下文中的啟動(dòng)時(shí)間統(tǒng)計(jì),均統(tǒng)計(jì)的是第二次啟動(dòng)后的數(shù)據(jù)姓蜂。(具體dyld緩存的是動(dòng)態(tài)庫(kù)而不是APP的可執(zhí)行代碼按厘,緩存的時(shí)間取決于內(nèi)核是否會(huì)將其丟棄,跟操作系統(tǒng)的頁(yè)面置換機(jī)制或內(nèi)存清理機(jī)制有關(guān))
見(jiàn)下圖钱慢,出處是這里:
其實(shí)在我們APP的實(shí)踐過(guò)程中也會(huì)遇到類似的事情逮京,只不過(guò)我只統(tǒng)計(jì)了第二次啟動(dòng)后的時(shí)間,也就是定義中的熱啟動(dòng)時(shí)間滩字。
注:
通過(guò)在工程的scheme中添加環(huán)境變量DYLD_PRINT_STATISTICS
造虏,設(shè)置值為1,App啟動(dòng)加載時(shí)Xcode的控制臺(tái)就會(huì)有pre-main各個(gè)階段的詳細(xì)耗時(shí)輸出麦箍。但是DYLD_PRINT_STATISTICS
變量打印時(shí)間是iOS10以后才支持的功能漓藕,所以需要用iOS10系統(tǒng)及以上的機(jī)器來(lái)做測(cè)試。
pre-main階段耗時(shí)的影響因素:
動(dòng)態(tài)庫(kù)加載越多挟裂,啟動(dòng)越慢享钞。
ObjC類越多,函數(shù)越多诀蓉,啟動(dòng)越慢栗竖。
可執(zhí)行文件越大啟動(dòng)越慢。
C的constructor函數(shù)越多渠啤,啟動(dòng)越慢狐肢。
C++靜態(tài)對(duì)象越多,啟動(dòng)越慢沥曹。
ObjC的+load越多份名,啟動(dòng)越慢碟联。
整體上pre-main階段的優(yōu)化有:
①減少依賴不必要的庫(kù),不管是動(dòng)態(tài)庫(kù)還是靜態(tài)庫(kù)僵腺;如果可以的話鲤孵,把動(dòng)態(tài)庫(kù)改造成靜態(tài)庫(kù);
如果必須依賴動(dòng)態(tài)庫(kù)辰如,則把多個(gè)非系統(tǒng)的動(dòng)態(tài)庫(kù)合并成一個(gè)動(dòng)態(tài)庫(kù)普监;
②檢查下 framework應(yīng)當(dāng)設(shè)為optional和required,
如果該framework在當(dāng)前App支持的所有iOS系統(tǒng)版本都存在琉兜,那么就設(shè)為required凯正,否則就設(shè)為optional,
因?yàn)閛ptional會(huì)有些額外的檢查豌蟋;
③合并或者刪減一些OC類和函數(shù)漆际;
關(guān)于清理項(xiàng)目中沒(méi)用到的類,使用工具AppCode代碼檢查功能夺饲,查到當(dāng)前項(xiàng)目中沒(méi)有用到的類(也可以用根據(jù)linkmap文件來(lái)分析奸汇,但是準(zhǔn)確度不算很高);
有一個(gè)叫做[FUI](https://github.com/dblock/fui)的開(kāi)源項(xiàng)目能很好的分析出不再使用的類往声,準(zhǔn)確率非常高擂找,唯一的問(wèn)題是它處理不了動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)里提供的類,也處理不了C++的類模板浩销。
④刪減一些無(wú)用的靜態(tài)變量贯涎,
⑤刪減沒(méi)有被調(diào)用到或者已經(jīng)廢棄的方法,
方法見(jiàn)http://stackoverflow.com/questions/35233564/how-to-find-unused-code-in-xcode-7
和https://developer.Apple.com/library/ios/documentation/ToolsLanguages/Conceptual/Xcode_Overview/CheckingCodeCoverage.html慢洋。
⑥將不必須在+load方法中做的事情延遲到+initialize中塘雳,盡量不要用C++虛函數(shù)(創(chuàng)建虛函數(shù)表有開(kāi)銷)
⑦類和方法名不要太長(zhǎng):iOS每個(gè)類和方法名都在__cstring段里都存了相應(yīng)的字符串值,所以類和方法名的長(zhǎng)短也是對(duì)可執(zhí)行文件大小是有影響的普筹;
因還是object-c的動(dòng)態(tài)特性败明,因?yàn)樾枰ㄟ^(guò)類/方法名反射找到這個(gè)類/方法進(jìn)行調(diào)用,object-c對(duì)象模型會(huì)把類/方法名字符串都保存下來(lái)太防;
⑧用dispatch_once()代替所有的 attribute((constructor)) 函數(shù)妻顶、C++靜態(tài)對(duì)象初始化、ObjC的+load函數(shù)蜒车;
⑨在設(shè)計(jì)師可接受的范圍內(nèi)壓縮圖片的大小讳嘱,會(huì)有意外收獲。
壓縮圖片為什么能加快啟動(dòng)速度呢酿愧?因?yàn)閱?dòng)的時(shí)候大大小小的圖片加載個(gè)十來(lái)二十個(gè)是很正常的沥潭,
圖片小了,IO操作量就小了嬉挡,啟動(dòng)當(dāng)然就會(huì)快了钝鸽,比較靠譜的壓縮算法是TinyPNG呼渣。
我們的實(shí)踐
統(tǒng)計(jì)了各個(gè)庫(kù)所占的size(使用之前做安裝包size優(yōu)化的一個(gè)腳本),基本上一個(gè)公共庫(kù)越大寞埠,類越多,啟動(dòng)時(shí)在pre-main階段所需要的時(shí)間也越多焊夸。
所以去掉了Realm仁连,DiffMatchPatch源碼庫(kù),以及AlicloudHttpDNS阱穗,BCConnectorBundl饭冬,F(xiàn)eedBack,SGMain和SecurityGuardSDK幾個(gè)庫(kù)揪阶;
結(jié)果如下:
靜態(tài)庫(kù)昌抠,少了7M左右:
第三方framework(其實(shí)也是靜態(tài)庫(kù),只是腳本會(huì)分開(kāi)統(tǒng)計(jì)而已)鲁僚,少了1M左右:
我們使用cocoapodbs并沒(méi)有設(shè)置use_frameworks炊苫,所以pod管理的有源碼的第三方庫(kù)都是靜態(tài)庫(kù)的形式,而framework形式的靜態(tài)庫(kù)基本都是第三方公司提供的服務(wù)冰沙,上圖可以看到侨艾,size占比最大的還是阿里和騰訊兩家的SDK,比如阿里的推送和騰訊的直播和IM拓挥。
上圖在統(tǒng)計(jì)中唠梨,AliCloudHttpDNS的可執(zhí)行文件在Mac的Finder下的大小大概是10M,AlicloudUtils是3.4M侥啤,UTMini是16M当叭,而UTDID只有1.6M。依賴關(guān)系上盖灸,AliCloudHttpDNS依賴AlicloudUtils蚁鳖,而AlicloudUtils依賴UTMini和UTDID,UTMini依賴UTDID赁炎。
上圖中在統(tǒng)計(jì)上才睹,應(yīng)該是有所AlicloudUtils在左右兩個(gè)圖中size差值過(guò)大,應(yīng)該是依賴關(guān)系中UTMini導(dǎo)致的統(tǒng)計(jì)偏差甘邀。兩邊幾個(gè)庫(kù)加起來(lái)的差值大概是200kb琅攘,這也應(yīng)該是AlicloudHttpDNS這個(gè)庫(kù)所占的size大小。
main階段
總體原則無(wú)非就是減少啟動(dòng)的時(shí)候的步驟松邪,以及每一步驟的時(shí)間消耗坞琴。
main階段的優(yōu)化大致有如下幾個(gè)點(diǎn):
①減少啟動(dòng)初始化的流程,能懶加載的就懶加載逗抑,能放后臺(tái)初始化的就放后臺(tái)剧辐,
能夠延時(shí)初始化的就延時(shí)寒亥,不要卡主線程的啟動(dòng)時(shí)間,已經(jīng)下線的業(yè)務(wù)直接刪掉荧关;
②優(yōu)化代碼邏輯溉奕,去除一些非必要的邏輯和代碼,減少每個(gè)流程所消耗的時(shí)間忍啤;
③啟動(dòng)階段使用多線程來(lái)進(jìn)行初始化加勤,把CPU的性能盡量發(fā)揮出來(lái);
④使用純代碼而不是xib或者storyboard來(lái)進(jìn)行UI框架的搭建同波,尤其是主UI框架比如TabBarController這種鳄梅,
盡量避免使用xib和storyboard,因?yàn)閤ib和storyboard也還是要解析成代碼來(lái)渲染頁(yè)面未檩,多了一些步驟戴尸;
這里重點(diǎn)講一下多線程啟動(dòng)設(shè)計(jì)的原理
首先,iPhone有雙核冤狡,除了維持操作系統(tǒng)運(yùn)轉(zhuǎn)和后臺(tái)進(jìn)程(包括電話短信等守護(hù)進(jìn)程)孙蒙,在打開(kāi)APP時(shí),猜想雙核中的另一個(gè)核應(yīng)該有余力來(lái)幫忙分擔(dān)啟動(dòng)的任務(wù)【待驗(yàn)證】悲雳;
其次马篮,iPhone7開(kāi)始使用A10,iPhone8開(kāi)始使用A11處理器怜奖,根據(jù)維基百科的定義浑测,A10 CPU包括兩枚高性能核心及兩枚低功耗核心,A11則包括兩個(gè)高性能核心和四個(gè)高能效核心歪玲,而且相比A10迁央,A11兩個(gè)性能核心的速度提升了25%,四個(gè)能效核心的速度提升了70%滥崩。而且蘋果還準(zhǔn)備了第二代性能控制器岖圈,因此可以同時(shí)發(fā)揮六個(gè)核心的全部威力,性能提升最高可達(dá)70%钙皮,多線程能力可見(jiàn)一斑蜂科。
多線程測(cè)試結(jié)果如下圖:
結(jié)論如下:
1.純算法的CPU運(yùn)算,指定計(jì)算的總數(shù)量短条,對(duì)于iPhone6和iPhone X來(lái)說(shuō)导匣,
把計(jì)算量平均分配到多線程比全部放在主線程執(zhí)行要快;
2.iPhone6三個(gè)線程跟兩個(gè)線程的總體耗時(shí)總體一致,甚至要多一點(diǎn)點(diǎn)茸时,
所以對(duì)于iPhone6來(lái)說(shuō)用兩個(gè)線程來(lái)做啟動(dòng)設(shè)計(jì)足夠了贡定;
3.iPhone X三個(gè)線程的耗時(shí)要比兩個(gè)線程短一些,但是差值已經(jīng)不算太大了可都;
四個(gè)線程跟三個(gè)線程的總體一致缓待,偶爾能比三個(gè)線程快一點(diǎn)點(diǎn)蚓耽;
綜上,利用多個(gè)線程來(lái)加速啟動(dòng)時(shí)間的設(shè)計(jì)旋炒,是合理的步悠。
但是多線程啟動(dòng)的設(shè)計(jì)有幾個(gè)需要注意的點(diǎn):
1.黑屏問(wèn)題;
2.用狀態(tài)機(jī)來(lái)設(shè)計(jì)瘫镇,每個(gè)狀態(tài)機(jī)有2或3個(gè)線程在跑不同的任務(wù)鼎兽,所有線程任務(wù)都完成后,進(jìn)入到下一個(gè)狀態(tài)汇四,方便擴(kuò)展;
3.線程碧哂浚活問(wèn)題通孽,以及用完后要銷毀;
4.資源競(jìng)爭(zhēng)或線程同步造成卡死的問(wèn)題睁壁。
針對(duì)第一點(diǎn)背苦,多線程跑初始化任務(wù)的時(shí)候,可能主線程會(huì)有空閑等待子線程的階段潘明,而主線程一旦空閑行剂,iOS系統(tǒng)的啟動(dòng)畫(huà)面就會(huì)消失,如果此時(shí)APP尚未初始化完全钳降,則可能會(huì)黑屏厚宰。為了避免黑屏,我們需要一個(gè)假的啟動(dòng)畫(huà)面遂填,在剛開(kāi)始跑初始化任務(wù)時(shí)铲觉,就生成這個(gè)啟動(dòng)畫(huà)面,啟動(dòng)過(guò)程完全結(jié)束后再去掉吓坚∧煊模或者當(dāng)一個(gè)狀態(tài)機(jī)里的主線程跑完時(shí)檢查下是否所有線程都執(zhí)行完任務(wù)了,如果沒(méi)有則去生成這個(gè)假的初始化頁(yè)面礁击,避免黑屏(我們采用后一種方式)盐杂。
第二點(diǎn)用狀態(tài)機(jī)來(lái)設(shè)計(jì)啟動(dòng),每個(gè)狀態(tài)跑兩個(gè)或者多個(gè)線程哆窿,單個(gè)狀態(tài)里每個(gè)線程的耗時(shí)是不一樣的链烈,跑完一個(gè)狀態(tài)再繼續(xù)下一個(gè)狀態(tài),可以多次測(cè)試去動(dòng)態(tài)調(diào)整分派到各個(gè)線程里的任務(wù)挚躯。
第三點(diǎn)線程辈舛猓活則跟runloop有關(guān),每個(gè)線程啟動(dòng)后秧均,跑完一個(gè)狀態(tài)食侮,不能立馬就回收号涯,不然下一個(gè)狀態(tài)的子線程就永遠(yuǎn)不會(huì)執(zhí)行了;另外就是锯七,APP初始化完成后链快,線程要注意回收。
第四點(diǎn)跟具體的業(yè)務(wù)有關(guān)眉尸,只要不是一個(gè)線程去做初始化域蜗,就有可能遇到線程間死鎖的問(wèn)題,比如下面采坑記錄里就有提到一個(gè)例子噪猾。
我們?cè)趯?shí)踐中大概做了以下的幾點(diǎn):
1.把啟動(dòng)時(shí)RN包的刪除和拷貝操作霉祸,僅在APP安裝后第一次啟動(dòng)時(shí)才做,之后的啟動(dòng)不再做這操作袱蜡,
而是等到網(wǎng)絡(luò)請(qǐng)求RN數(shù)據(jù)回來(lái)丝蹭,根據(jù)是否需要更新RN包的判斷,再去做這些IO操作從而避免啟動(dòng)的耗時(shí)坪蚁。
iPhone5C能節(jié)省1.4s奔穿;
2.OSS token的獲取不是一個(gè)需要在啟動(dòng)的時(shí)候必須要做的操作,放到子線程去處理敏晤,大部分時(shí)候是節(jié)省10-15ms贱田,偶爾能去到50ms;
3.去掉啟動(dòng)狀態(tài)機(jī)里的原有定位服務(wù)嘴脾,原來(lái)SSZLocationManager的定位服務(wù)因?yàn)閮?nèi)部依賴高德的SDK男摧,
需要初始化SDK,iPhone5C大概耗時(shí)100ms译打。同時(shí)SSZLocationManager這個(gè)類代碼保留彩倚,
但是APP的工程去除對(duì)其的依賴;
4.打點(diǎn)統(tǒng)計(jì)模塊里的定位服務(wù)權(quán)限請(qǐng)求改成異步扶平,大概有50ms帆离;
5.阿里百川的Feedback,在網(wǎng)校并沒(méi)有使用结澄,直接去掉其初始化流程哥谷,大概5ms左右;
6.友盟的分享服務(wù)麻献,沒(méi)有必要在啟動(dòng)的時(shí)候去初始化们妥,初始化任務(wù)丟到后臺(tái)線程解決,大概600-800ms勉吻;
7.狀態(tài)機(jī)跑完后的啟動(dòng)內(nèi)存統(tǒng)計(jì)监婶,放到后臺(tái)去做,大概50ms;
8.UserAgentManager里對(duì)于webview是否為UIWebview的判斷惑惶,以前是新創(chuàng)建一個(gè)對(duì)象使用對(duì)象方法來(lái)判斷煮盼,
修改為直接使用類方法,避免創(chuàng)建對(duì)象的消耗带污,節(jié)省約200ms僵控;
9.阿里云的HTTPDNS已經(jīng)沒(méi)有再使用了,所以這里也可以直接去掉鱼冀。大概20-40ms报破;
10.SSZAppConfig里把網(wǎng)絡(luò)請(qǐng)求放到后臺(tái)線程,這樣子就可以配合啟動(dòng)狀態(tài)機(jī)把該任務(wù)放到子線程進(jìn)行初始化了千绪,
否則子線程消耗的時(shí)間太長(zhǎng)了充易;
11.采用兩個(gè)線程來(lái)進(jìn)行啟動(dòng)流程的初始化,用狀態(tài)機(jī)來(lái)控制狀態(tài)的變化荸型。
但是要針對(duì)業(yè)務(wù)區(qū)分開(kāi)盹靴,并不是把一部分業(yè)務(wù)拆分到子線程,就可以讓整體的啟動(dòng)速度更快帆疟;
因?yàn)槿绻泳€程有一些操作是要在主線程做的鹉究,有可能會(huì)出現(xiàn)等待主線程空閑再繼續(xù)的情況宇立;
或者當(dāng)兩個(gè)線程的耗時(shí)操作都是IO時(shí)踪宠,拆開(kāi)到兩個(gè)線程,并不一定比單個(gè)線程去做IO操作要快妈嘹。
12.主UI框架tabBarController的viewDidLoad函數(shù)里柳琢,去掉一些不必要的函數(shù)調(diào)用。
13.NSUserDefaults的synchronize函數(shù)盡量不要在啟動(dòng)流程中去調(diào)用润脸,統(tǒng)一在APP進(jìn)入后臺(tái)柬脸,
willTerminate和完全進(jìn)入前臺(tái)后把數(shù)據(jù)落地;
因?yàn)槲覀兊捻?xiàng)目用到了React Native技術(shù)(簡(jiǎn)稱RN)毙驯,所以會(huì)有RN包的拷貝和更新這一操作倒堕,之前的邏輯是每次啟動(dòng)都從bundle里拷貝一次到Document的指定目錄,本次優(yōu)化修正為除了安裝后第一次啟動(dòng)拷貝爆价,其他時(shí)候不在做這個(gè)拷貝操作垦巴,不過(guò)RN包熱更新的覆蓋操作,還是每次都要做檢查的铭段,如果有需要?jiǎng)t執(zhí)行更新操作
其中遇到幾個(gè)坑:
①并不是什么任務(wù)都適合放子線程骤宣,有些任務(wù)在主線程大概10ms,放到子線程需要幾百ms序愚,因?yàn)槟承┤蝿?wù)內(nèi)部可能會(huì)用到UIKit的api憔披,又或者某些操作是需要提交到主線程去執(zhí)行的,關(guān)鍵是要搞清楚這個(gè)任務(wù)里邊究竟做了啥,有些SDK并不見(jiàn)得適合放到子線程去初始化芬膝,需要具體情況具體去測(cè)試和分析望门。
②AccountManager初始化導(dǎo)致的主線程卡死,子線程的任務(wù)依賴AccountManager蔗候,主線程也依賴怒允,當(dāng)子線程比主線程先調(diào)用時(shí),造成了主線程卡死锈遥,其原因是子線程會(huì)提交一個(gè)同步阻塞的操作到主線程纫事,而此時(shí)主線程被dipatch_one的鎖鎖住了,所以造成子線程不能返回所灸,主線程也無(wú)法繼續(xù)執(zhí)行丽惶。調(diào)試的時(shí)候還會(huì)用到符號(hào)斷點(diǎn)和LLDB去打印函數(shù)入?yún)ⅲㄒ话闶莚0-r3之間的寄存器)的值。
③RN包的拷貝檢查除了是否第一次打開(kāi)APP之外爬立,還要注意RN版本如果升級(jí)時(shí)钾唬,需要用新的包強(qiáng)制覆蓋掉舊的包,否則js代碼會(huì)一直得不到更新侠驯。
實(shí)際優(yōu)化效果對(duì)比
由于只是去掉了幾個(gè)靜態(tài)庫(kù)抡秆,而且本來(lái)pre-main階段的耗時(shí)就不長(zhǎng),基本在200ms-500ms左右吟策,所以pre-main階段優(yōu)化前后效果并不明顯儒士,有時(shí)候還沒(méi)有前后測(cè)試的誤差大。檩坚。着撩。
main的階段的優(yōu)化效果還是很明顯的:
iPhone5C iOS10.3.3系統(tǒng)優(yōu)化前main階段的時(shí)間消耗為4秒左右,優(yōu)化后基本在1.8秒內(nèi)匾委;
iPhone7 iOS10.3.3系統(tǒng)優(yōu)化前main階段的時(shí)間消耗為1.1秒左右拖叙,優(yōu)化后基本在600ms內(nèi);
iPhoneX iOS11.3.1系統(tǒng)優(yōu)化前main階段的時(shí)間消耗基本在1.5秒以上赂乐,優(yōu)化后在1秒內(nèi)薯鳍;
可以看到,同樣arm64架構(gòu)的機(jī)器挨措,main階段是iPhone7比iPhoneX更快挖滤,說(shuō)明就操作系統(tǒng)來(lái)說(shuō),iOS11.3要比iOS10.3慢不少运嗜;
詳細(xì)測(cè)試數(shù)據(jù)見(jiàn)下圖
上圖中iPhone5C為啥前后測(cè)試pre-main時(shí)間差值那么大壶辜?而且是優(yōu)化后的值比優(yōu)化前還要大?我也不知道担租,大概機(jī)器自己才知道吧砸民。。。
注意:
1.關(guān)于冷啟動(dòng)和熱啟動(dòng)岭参,業(yè)界對(duì)冷啟動(dòng)的定義沒(méi)有問(wèn)題反惕,普遍認(rèn)為是手機(jī)開(kāi)機(jī)后第一次啟動(dòng)某個(gè)APP,但是對(duì)熱啟動(dòng)有不同的看法演侯,有些人認(rèn)為是按下home鍵把APP掛到后臺(tái)姿染,之后點(diǎn)擊APP的icon再拉回來(lái)到前臺(tái)算是熱啟動(dòng),也有些人認(rèn)為是手機(jī)開(kāi)機(jī)后在短時(shí)間內(nèi)第二次啟動(dòng)APP(殺掉進(jìn)程重啟)算是熱啟動(dòng)(此時(shí)dyld會(huì)對(duì)部分APP的數(shù)據(jù)和庫(kù)進(jìn)行緩存秒际,所以比第一次啟動(dòng)要快)悬赏。筆者認(rèn)為APP從后臺(tái)拉起到前臺(tái)的時(shí)間沒(méi)啥研究的意義,而即使是短時(shí)間內(nèi)第二次啟動(dòng)APP娄徊,啟動(dòng)時(shí)間也是很重要的闽颇,所以在統(tǒng)計(jì)啟動(dòng)時(shí)間時(shí),筆者會(huì)傾向于后一種說(shuō)法寄锐,不過(guò)具體怎么定義還是看個(gè)人吧兵多,知道其中的區(qū)別就好。
2.關(guān)于如何區(qū)分framework是靜態(tài)庫(kù)還是動(dòng)態(tài)庫(kù)見(jiàn)這里橄仆。原理就是在終端使用指令file剩膘,輸出如果是ar archive就是靜態(tài)庫(kù),如果是動(dòng)態(tài)庫(kù)則會(huì)輸出dynamically linked相關(guān)信息盆顾。
參考鏈接:
1.優(yōu)化 App 的啟動(dòng)時(shí)間;
4.今日頭條iOS客戶端啟動(dòng)速度優(yōu)化;
5.XNU怠褐、dyld源碼分析Mach-O和動(dòng)態(tài)庫(kù)的加載過(guò)程(上);