背景
iOS的啟動過程一直比較神秘,這方面的資料也不是太多,大多數(shù)的資料都來自2016年WWDC的一篇視頻,本文的大部分內(nèi)容來自于視頻,算是視頻的一個(gè)歸納總結(jié)再加上自己的一點(diǎn)點(diǎn)感悟吧。
啟動的過程
dyld是App的啟動器,啟動的大部分事情都由dyld完成,iOS的啟動大致分為幾個(gè)部分:
內(nèi)核將App的執(zhí)行文件加載到隨機(jī)地址空間(加載到隨機(jī)地址主要是因?yàn)?a target="_blank">ASLR技術(shù))
內(nèi)核將dyld的執(zhí)行文件加載到隨機(jī)地址空間
內(nèi)核執(zhí)行dyld文件
dyld啟動App
dyld加載所有App所依賴的dylibs(動態(tài)庫)
執(zhí)行rebasing/binding修復(fù)地址
Objc Setup
initialize
dyld調(diào)用App中的main(),將主動權(quán)交還給App
手機(jī)內(nèi)核只負(fù)責(zé)將App的執(zhí)行文件和dyld加載到內(nèi)存中,然后所有的啟動工作都交給了dyld。
dyld加載App依賴的dylibs
dyld拿到App的執(zhí)行文件后,首先從文件的header中解析出App依賴的dylib列表,找到每一個(gè)依賴的dylib。打開并讀取dylib文件的起始位置,驗(yàn)證簽名,確保dylib沒有被篡改。驗(yàn)證簽名后,對dylib中的每個(gè)segment調(diào)用mmap()
segment
一般每個(gè)Mach-O文件都會有三個(gè)segment:
__TEXT: 一般處于文件的頭部位置,包含Mach header,被執(zhí)行的代碼,和只讀常量,只讀可執(zhí)行(r-x)。由于不會被更改,所以讀到內(nèi)存中后可復(fù)用
__DATA: 包含各種變量,可讀寫(rw-),由于可以被更改,所以不可復(fù)用
__LINKEDIT: 包含函數(shù)名稱和對應(yīng)的地址,只讀(r--)
mmap()
文件讀入內(nèi)存并不用一次性讀入整個(gè)文件,它可以使用分頁映射(mmap())的方式進(jìn)行讀取。也就是用到哪個(gè)segment,再將哪個(gè)segment讀入內(nèi)存,實(shí)現(xiàn)文件讀入的懶加載。
同時(shí)同一個(gè)Mach-O文件中的segment也可以映射到多個(gè)進(jìn)程,實(shí)現(xiàn)進(jìn)程之間的內(nèi)存共享。__TEXT和__LINKEDIT段都是只讀的,不會有進(jìn)程對它進(jìn)行修改,它們是可以讓所有進(jìn)程共享的,大家都使用同一份內(nèi)容。然而__DATA段卻不是這樣,__DATA是可讀寫的,當(dāng)某一個(gè)進(jìn)程需要對它進(jìn)行修改時(shí),需要先copy一份出來,映射到新的 RAM 頁上。讓這個(gè)進(jìn)程擁有自己獨(dú)立的內(nèi)存拷貝,進(jìn)行修改。這就是Copy-On-Write技術(shù),簡稱COW。
由于__TEXT和__LINKEDIT段可以進(jìn)程間共享,只需要在第一次使用的時(shí)候進(jìn)行IO操作,后續(xù)即可直接使用,所以App在第一次啟動時(shí)會比較費(fèi)時(shí),因?yàn)樗械膕egment讀取都需要進(jìn)行IO操作。后續(xù)啟動,會快很多,很多segment已經(jīng)映射到內(nèi)存中,會被緩存起來,二次啟動直接使用,不需要進(jìn)行IO操作,這就有了iOS中冷啟動和熱啟動的概念:
冷啟動:新安裝App或者手機(jī)重啟后,第一次啟動。手機(jī)需要加載所有的segment
熱啟動:啟動過App后,再次啟動。內(nèi)存中緩存的segment可以直接復(fù)用。
執(zhí)行rebasing/binding修復(fù)地址
由于App和每個(gè)dylib加載到的都是隨機(jī)地址空間,代碼中原來的函數(shù)地址跟真實(shí)的函數(shù)地址會有差異。修復(fù)這個(gè)差異的過程就是rebasing和binding。其中rebasing主要做的是image內(nèi)部的修復(fù),binding主要做的是image間的修復(fù)。
Rebasing
對于Image內(nèi)部的函數(shù),假設(shè)它的原地址是A,對應(yīng)當(dāng)前地址空間下的新地址是B。那么它所有的函數(shù)指針都需要加上地址差(B-A)。所有的Rebasing過程就是從__LINKEDIT取出函數(shù)指針,修改函數(shù)指針,存入__DATA中,供函數(shù)調(diào)用。(原始的函數(shù)指針存在__LINKEDIT中,修改后的數(shù)據(jù)存在__DATA中)
之前說到,加載文件使用的是mmap技術(shù),__LINKEDIT和DATA段是在第一次使用時(shí)才會執(zhí)行IO操作,加載到內(nèi)存中。所以Rebasing階段,耗時(shí)主要是在IO操作上。
Binding
image間的函數(shù)指針,實(shí)際是被符號名稱綁定的,為了找到對應(yīng)的函數(shù)實(shí)現(xiàn),dyld需要去符號表中根據(jù)符號名稱查找,找到后將地址存到__DATA中對應(yīng)函數(shù)指針中。由于IO操作在rebasing階段已經(jīng)在做了,所以binding階段主要耗時(shí)在符號表查找的這個(gè)過程,這個(gè)過程的主要瓶頸在CPU計(jì)算上。
Objc Setup
Objc是一門動態(tài)語言,為了維持它的動態(tài)性,在啟動時(shí),需要將類的名稱和類的方法都注冊起來。Objc Setup階段,主要是做Class的注冊,Method的注冊和Category的注冊。
一個(gè)好的設(shè)計(jì)模式,一般都推崇寫很多類,每個(gè)類盡量簡單,寫很多Category,每個(gè)Category都只包含獨(dú)立模塊的方法。但是從啟動速度的角度來說,盡量減少類,Category和方法,才會讓Objc Setup階段耗時(shí)更少。
initialize
當(dāng)所有的Class和method都注冊過后,系統(tǒng)需要做一些初始化的工作,對于Objective-C而言,主要是需要調(diào)用各個(gè)類的+load方法,所以項(xiàng)目中應(yīng)該盡量避免使用+load方法,正常的初始化工作,可以在initialize中實(shí)現(xiàn)。StackOverflow上有詳細(xì)的關(guān)于+load和initialize的對比
End
當(dāng)上面所有階段執(zhí)行完成之后,dyld會調(diào)用main()函數(shù),將主動權(quán)交還給App。之后才會調(diào)用到didFinishLaunch中的代碼。
上面介紹的啟動時(shí)間主要是main()函數(shù)之前的啟動時(shí)間,正常這個(gè)時(shí)間控制在400ms以內(nèi)就可以算一個(gè)啟動速度優(yōu)異的App了。正常我們關(guān)注更多的可能是main()函數(shù)后didFinishLaunch中代碼的執(zhí)行時(shí)間。但是對用戶而言,main()函數(shù)之前的時(shí)間也是啟動的一部分。往往這部分時(shí)間也不短,所以不能掉以輕心哦~
作者:小笨狼
鏈接:http://www.itdecent.cn/p/4fe773d6da4c#comments