App啟動分成兩部分
- pre-main 階段的定義為 APP 開始啟動到系統調用 main 函數這一段時間
- main 階段則代表從main函數入口到主 UI 框架的 viewDidAppear
Pre-main
dyld:dynamic loader,它的作用是加載一個進程所需要的image,dyld是開源的
Pre-main的流程.png
Load Dylibs
dyld加載App的可執行文件,App內嵌的庫,系統動態庫。每個庫,就是Mach-o文件,其頭部都包含依賴庫信息,dyld會遞歸加載這些庫。
- 系統庫會有緩存,dyld共享庫緩存,加載很快
- 內嵌的庫加載較慢
加載過程
- 分析所依賴的動態庫
- 找到動態庫的mach-o文件
- 打開文件
- 驗證文件
- 在系統核心注冊文件簽名
- 對動態庫的每一個segment調用mmap()
優化:
- 減少非系統庫的依賴
- 使用靜態庫而不是動態庫
- 合并多個非系統動態庫為一個動態庫
Rebase && Binding
ASLR的全稱是Address space layout randomization,翻譯過來就是“地址空間布局隨機化”。App被啟動的時候,程序會被影射到邏輯的地址空間,這個邏輯的地址空間有一個起始地址,而ASLR技術使得這個起始地址是隨機的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函數的地址。
- Rebase 修正內部(指向當前mach-o文件)的指針指向。是因為剛剛提到的ASLR使得地址隨機化,導致起始地址不固定,另外由于Code Sign,導致不能直接修改Image。Rebase的時候只需要增加對應的偏移量即可。待Rebase的數據都存放在__LINKEDIT中
-
Bind 修正外部指針指向。外部的符號引用則是由Bind解決。在解決Bind的時候,是根據字符串匹配的方式查找符號表,所以這個過程相對于Rebase來說是略慢的
Rebase && Binding.png
優化
- 減少Objc類數量, 減少selector數量,把未使用的類和函數都可以刪掉。跟單職責有點沖突?
- 減少C++虛函數數量
- 轉而使用swift stuct(其實本質上就是為了減少符號的數量,使用swift語言來開發?)
OBJC Runtime Setup
- 讀取二進制文件的 DATA 段內容,找到與 objc 相關的信息
- 注冊 Objc 類,ObjC Runtime 需要維護一張映射類名與類的全局表。當加載一個 dylib 時,其定義的所有的類都需要被注冊到這個全局表中;
- 讀取 protocol 以及 category 的信息,把category的定義插入方法列表 (category registration),
- 確保 selector 的唯一性
Initializers
以上三步屬于靜態調整,都是在修改__DATA segment中的內容,而這里則開始動態調整,開始在堆和棧中寫入內容。 工作主要有:
- Objc的+load()函數
- C++的構造函數屬性函數 形如attribute((constructor)) void DoSomeInitializationWork()
- 非基本類型的C++靜態全局變量的創建。(通常是類或結構體)(non-trivial initializer) 比如一個全局靜態結構體的構建,如果在構造函數中有繁重的工作,那么會拖慢啟動速度
優化
- 使用 +initialize 來替代 +load
- 不要使用 atribute((constructor)) 將方法顯式標記為初始化器,而是讓初始化方法調用時才執行。
- 比如使用
dispatch_once()
,pthread_once()
或std::once()
。也就是在第一次使用時才初始化,推遲了一部分工作耗時。 - 也盡量不要用到C++的靜態對象。
- 比如使用
Pre-main 總結
-
pre-main階段耗時的影響因素:
- 動態庫加載越多,啟動越慢。
- ObjC類越多,函數越多,啟動越慢。
- 可執行文件越大啟動越慢。
- C的constructor函數越多,啟動越慢。
- C++靜態對象越多,啟動越慢。
- ObjC的+load越多,啟動越慢。
-
整體上pre-main階段的優化有:
- 減少依賴不必要的庫,不管是動態庫還是靜態庫;如果可以的話,把動態庫改造成靜態庫;如果必須依賴動態庫,則把多個非系統的動態庫合并成一個動態庫;
- 檢查下 framework應當設為optional和required,如果該framework在當前App支持的所有iOS系統版本都存在,那么就設為required,否則就設為optional,因為optional會有些額外的檢查;
- 合并或者刪減一些OC類和函數;關于清理項目中沒用到的類,使用工具AppCode代碼檢查功能,查到當前項目中沒有用到的類(也可以用根據linkmap文件來分析,但是準確度不算很高);有一個叫做FUI的開源項目能很好的分析出不再使用的類,準確率非常高,唯一的問題是它處理不了動態庫和靜態庫里提供的類,也處理不了C++的類模板。
- 刪減一些無用的靜態變量,
- 刪減沒有被調用到或者已經廢棄的方法
- 將不必須在+load方法中做的事情延遲到+initialize中,盡量不要用C++虛函數(創建虛函數表有開銷)
- 類和方法名不要太長:iOS每個類和方法名都在__cstring段里都存了相應的字符串值,所以類和方法名的長短也是對可執行文件大小是有影響的;因還是object-c的動態特性,因為需要通過類/方法名反射找到這個類/方法進行調用,object-c對象模型會把類/方法名字符串都保存下來;
- 用dispatch_once()代替所有的 attribute((constructor)) 函數、C++靜態對象初始化、ObjC的+load函數;
- 在設計師可接受的范圍內壓縮圖片的大小,會有意外收獲。壓縮圖片為什么能加快啟動速度呢?因為啟動的時候大大小小的圖片加載個十來二十個是很正常的,圖片小了,IO操作量就小了,啟動當然就會快了,比較靠譜的壓縮算法是TinyPNG。
Main
總體原則無非就是減少啟動的時候的步驟,以及每一步驟的時間消耗。
main階段的優化大致有如下幾個點:
- 減少啟動初始化的流程,能懶加載的就懶加載,能放后臺初始化的就放后臺,能夠延時初始化的就延時,不要卡主線程的啟動時間,已經下線的業務直接刪掉;
- 優化代碼邏輯,去除一些非必要的邏輯和代碼,減少每個流程所消耗的時間;
- 啟動階段使用多線程來進行初始化,把CPU的性能盡量發揮出來;
- 使用純代碼而不是xib或者storyboard來進行UI框架的搭建,尤其是主UI框架比如TabBarController這種,盡量避免使用xib和storyboard,因為xib和storyboard也還是要解析成代碼來渲染頁面,多了一些步驟;
- 主UI框架tabBarController的viewDidLoad函數里,去掉一些不必要的函數調用
其中遇到幾個坑:
- 并不是什么任務都適合放子線程,有些任務在主線程大概10ms,放到子線程需要幾百ms,因為某些任務內部可能會用到UIKit的api,又或者某些操作是需要提交到主線程去執行的,關鍵是要搞清楚這個任務里邊究竟做了啥,有些SDK并不見得適合放到子線程去初始化,需要具體情況具體去測試和分析。
實際優化效果
由于只是去掉了幾個靜態庫,而且本來pre-main階段的耗時就不長,基本在200ms-500ms左右,所以pre-main階段優化前后效果并不明顯,有時候還沒有前后測試的誤差大。。。
main的階段的優化效果還是很明顯的:
- iPhone5C iOS10.3.3系統優化前main階段的時間消耗為4秒左右,優化后基本在1.8秒內;
- iPhone7 iOS10.3.3系統優化前main階段的時間消耗為1.1秒左右,優化后基本在600ms內;
- iPhoneX iOS11.3.1系統優化前main階段的時間消耗基本在1.5秒以上,優化后在1秒內;
可以看到,同樣arm64架構的機器,main階段是iPhone7比iPhoneX更快,說明就操作系統來說,iOS11.3要比iOS10.3慢不少;