iOS客戶端啟動優化

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共享庫緩存,加載很快
  • 內嵌的庫加載較慢

加載過程

  1. 分析所依賴的動態庫
  2. 找到動態庫的mach-o文件
  3. 打開文件
  4. 驗證文件
  5. 在系統核心注冊文件簽名
  6. 對動態庫的每一個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 總結

  1. pre-main階段耗時的影響因素:

    • 動態庫加載越多,啟動越慢。
    • ObjC類越多,函數越多,啟動越慢。
    • 可執行文件越大啟動越慢。
    • C的constructor函數越多,啟動越慢。
    • C++靜態對象越多,啟動越慢。
    • ObjC的+load越多,啟動越慢。
  2. 整體上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慢不少;

參考文章

  1. 今日頭條 http://www.cocoachina.com/ios/20170208/18651.html
  2. http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/
  3. 綜合 https://mp.weixin.qq.com/s/zeWfmAi0YnoQowcPpFhHUA
  4. https://blog.csdn.net/hello_hwc/article/details/78317863,黃文臣
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容