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