背景
iOS的啟動過程一直比較神秘捎迫,這方面的資料也不是太多晃酒,大多數(shù)的資料都來自2016年WWDC的一篇視頻,本文的大部分內(nèi)容來自于視頻窄绒,算是視頻的一個歸納總結(jié)再加上自己的一點點感悟吧贝次。
啟動的過程
dyld是App的啟動器,啟動的大部分事情都由dyld完成彰导,iOS的啟動大致分為幾個部分:
- 內(nèi)核將App的執(zhí)行文件加載到隨機地址空間(加載到隨機地址主要是因為ASLR技術(shù))
- 內(nèi)核將dyld的執(zhí)行文件加載到隨機地址空間
- 內(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
手機內(nèi)核只負責(zé)將App的執(zhí)行文件和dyld加載到內(nèi)存中,然后所有的啟動工作都交給了dyld位谋。
dyld加載App依賴的dylibs
dyld拿到App的執(zhí)行文件后山析,首先從文件的header中解析出App依賴的dylib列表,找到每一個依賴的dylib掏父。打開并讀取dylib文件的起始位置笋轨,驗證簽名,確保dylib沒有被篡改赊淑。驗證簽名后爵政,對dylib中的每個segment調(diào)用mmap()
segment
一般每個Mach-O文件都會有三個segment:
__TEXT
: 一般處于文件的頭部位置,包含Mach header陶缺,被執(zhí)行的代碼钾挟,和只讀常量,只讀可執(zhí)行(r-x)饱岸。由于不會被更改掺出,所以讀到內(nèi)存中后可復(fù)用
__DATA
: 包含各種變量,可讀寫(rw-)苫费,由于可以被更改汤锨,所以不可復(fù)用
__LINKEDIT
: 包含函數(shù)名稱和對應(yīng)的地址,只讀(r--)
mmap()
文件讀入內(nèi)存并不用一次性讀入整個文件百框,它可以使用分頁映射(mmap())的方式進行讀取泥畅。也就是用到哪個segment,再將哪個segment讀入內(nèi)存,實現(xiàn)文件讀入的懶加載位仁。
同時同一個Mach-O文件中的segment也可以映射到多個進程,實現(xiàn)進程之間的內(nèi)存共享方椎。__TEXT
和__LINKEDIT
段都是只讀的聂抢,不會有進程對它進行修改,它們是可以讓所有進程共享的棠众,大家都使用同一份內(nèi)容琳疏。然而__DATA
段卻不是這樣,__DATA
是可讀寫的闸拿,當(dāng)某一個進程需要對它進行修改時空盼,需要先copy一份出來,映射到新的 RAM 頁上新荤。讓這個進程擁有自己獨立的內(nèi)存拷貝揽趾,進行修改。這就是Copy-On-Write技術(shù)苛骨,簡稱COW篱瞎。
由于__TEXT
和__LINKEDIT
段可以進程間共享,只需要在第一次使用的時候進行IO操作痒芝,后續(xù)即可直接使用俐筋,所以App在第一次啟動時會比較費時,因為所有的segment讀取都需要進行IO操作严衬。后續(xù)啟動澄者,會快很多,很多segment已經(jīng)映射到內(nèi)存中请琳,會被緩存起來粱挡,二次啟動直接使用,不需要進行IO操作单起,這就有了iOS中冷啟動和熱啟動的概念:
- 冷啟動:新安裝App或者手機重啟后抱怔,第一次啟動。手機需要加載所有的segment
- 熱啟動:啟動過App后嘀倒,再次啟動屈留。內(nèi)存中緩存的segment可以直接復(fù)用。
執(zhí)行rebasing/binding修復(fù)地址
由于App和每個dylib加載到的都是隨機地址空間测蘑,代碼中原來的函數(shù)地址跟真實的函數(shù)地址會有差異灌危。修復(fù)這個差異的過程就是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
段是在第一次使用時才會執(zhí)行IO操作角溃,加載到內(nèi)存中拷获。所以Rebasing階段,耗時主要是在IO操作上减细。
Binding
image間的函數(shù)指針匆瓜,實際是被符號名稱綁定的,為了找到對應(yīng)的函數(shù)實現(xiàn)邪财,dyld需要去符號表中根據(jù)符號名稱查找陕壹,找到后將地址存到__DATA
中對應(yīng)函數(shù)指針中。由于IO操作在rebasing階段已經(jīng)在做了树埠,所以binding階段主要耗時在符號表查找的這個過程糠馆,這個過程的主要瓶頸在CPU計算上。
Objc Setup
Objc是一門動態(tài)語言怎憋,為了維持它的動態(tài)性又碌,在啟動時,需要將類的名稱和類的方法都注冊起來绊袋。Objc Setup階段毕匀,主要是做Class的注冊,Method的注冊和Category的注冊癌别。
一個好的設(shè)計模式皂岔,一般都推崇寫很多類,每個類盡量簡單展姐,寫很多Category躁垛,每個Category都只包含獨立模塊的方法。但是從啟動速度的角度來說圾笨,盡量減少類教馆,Category和方法,才會讓Objc Setup階段耗時更少擂达。
initialize
當(dāng)所有的Class和method都注冊過后土铺,系統(tǒng)需要做一些初始化的工作,對于Objective-C而言,主要是需要調(diào)用各個類的+load
方法悲敷,所以項目中應(yīng)該盡量避免使用+load
方法究恤,正常的初始化工作,可以在initialize
中實現(xiàn)后德。StackOverflow上有詳細的關(guān)于+load
和initialize
的對比
End
當(dāng)上面所有階段執(zhí)行完成之后丁溅,dyld會調(diào)用main()函數(shù),將主動權(quán)交還給App探遵。之后才會調(diào)用到didFinishLaunch中的代碼。
上面介紹的啟動時間主要是main()函數(shù)之前的啟動時間妓柜,正常這個時間控制在400ms以內(nèi)就可以算一個啟動速度優(yōu)異的App了箱季。正常我們關(guān)注更多的可能是main()函數(shù)后didFinishLaunch中代碼的執(zhí)行時間。但是對用戶而言棍掐,main()函數(shù)之前的時間也是啟動的一部分藏雏。往往這部分時間也不短,所以不能掉以輕心哦~