1. 程式人生 > >iOS端啟動速度優化

iOS端啟動速度優化

應用啟動流程


iOS應用的啟動可分為pre-main階段和main()階段,其中系統做的事情依次是:


1. pre-main階段

1.1. 載入應用的可執行檔案

1.2. 載入動態連結庫載入器dyld(dynamic loader)

1.3. dyld遞迴載入應用所有依賴的dylib(dynamic library 動態連結庫)


對於pre-main階段,Apple提供了一種測量各階段的耗時的方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環境變數DYLD_PRINT_STATISTICS 設為1 ,設定好後把程式跑起來,控制檯會有如下輸出:




2. main()階段

2.1. dyld呼叫main() 

2.2. 呼叫UIApplicationMain() 

2.3. 呼叫applicationWillFinishLaunching

2.4. 呼叫didFinishLaunchingWithOptions


對於main()階段,主要是測量main()函式開始執行到didFinishLaunchingWithOptions執行結束的耗時,就需要自己插入程式碼到工程中了。先在main()函式裡用變數StartTime記錄當前時間:


CFAbsoluteTime StartTime;

int main(int argc, char * argv[]) {

      StartTime = CFAbsoluteTimeGetCurrent();


再在AppDelegate.m檔案中用extern宣告全域性變數StartTime


extern CFAbsoluteTime StartTime;


最後在didFinishLaunchingWithOptions裡,再獲取一下當前時間,與StartTime的差值即是main()階段執行耗時。


double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);


pre-main階段的優化


要對pre-main階段的耗時做優化,需要再學習下dyld載入的過程,根據Apple在WWDC上的介紹,dyld的載入主要分為4步:


1. Load dylibs


這一階段dyld會分析應用依賴的dylib,找到其mach-o檔案,開啟和讀取這些檔案並驗證其有效性,接著會找到程式碼簽名註冊到核心,最後對dylib的每一個segment呼叫mmap()。


一般情況下,iOS應用會載入100-400個dylibs,其中大部分是系統庫,這部分dylib的載入系統已經做了優化。


所以,依賴的dylib越少越好。在這一步,我們可以做的優化有:


  1. 儘量不使用內嵌(embedded)的dylib,載入內嵌dylib效能開銷較大

  2. 合併已有的dylib和使用靜態庫(static archives),減少dylib的使用個數

  3. 懶載入dylib,但是要注意dlopen()可能造成一些問題,且實際上懶載入做的工作會更多


2. Rebase/Bind


在dylib的載入過程中,系統為了安全考慮,引入了ASLR(Address Space Layout Randomization)技術和程式碼簽名。由於ASLR的存在,映象(Image,包括可執行檔案、dylib和bundle)會在隨機的地址上載入,和之前指標指向的地址(preferred_address)會有一個偏差(slide),dyld需要修正這個偏差,來指向正確的地址。


Rebase在前,Bind在後,Rebase做的是將映象讀入記憶體,修正映象內部的指標,效能消耗主要在IO。Bind做的是查詢符號表,設定指向映象外部的指標,效能消耗主要在CPU計算。


所以,指標數量越少越好。在這一步,我們可以做的優化有:


  1. 減少ObjC類(class)、方法(selector)、分類(category)的數量

  2. 減少C++虛擬函式的的數量(建立虛擬函式表有開銷)

  3. 使用Swift structs(內部做了優化,符號數量更少)


3. Objc setup


大部分ObjC初始化工作已經在Rebase/Bind階段做完了,這一步dyld會註冊所有宣告過的ObjC類,將分類插入到類的方法列表裡,再檢查每個selector的唯一性。


在這一步倒沒什麼優化可做的,Rebase/Bind階段優化好了,這一步的耗時也會減少。


4. Initializers


到了這一階段,dyld開始執行程式的初始化函式,呼叫每個Objc類和分類的+load方法,呼叫C/C++ 中的構造器函式(用attribute((constructor))修飾的函式),和建立非基本型別的C++靜態全域性變數。Initializers階段執行完後,dyld開始呼叫main()函式。


在這一步,我們可以做的優化有:


  1. 少在類的+load方法裡做事情,儘量把這些事情推遲到+initiailize

  2. 減少構造器函式個數,在構造器函式裡少做些事情

  3. 減少C++靜態全域性變數的個數


main()階段的優化


這一階段的優化主要是減少didFinishLaunchingWithOptions方法裡的工作,在didFinishLaunchingWithOptions方法裡,我們會建立應用的window,指定其rootViewController,呼叫window的makeKeyAndVisible方法讓其可見。由於業務需要,我們會初始化各個二方/三方庫,設定系統UI風格,檢查是否需要顯示引導頁、是否需要登入、是否有新版本等,由於歷史原因,這裡的程式碼容易變得比較龐大,啟動耗時難以控制。


所以,滿足業務需要的前提下,didFinishLaunchingWithOptions在主執行緒裡做的事情越少越好。在這一步,我們可以做的優化有:


  1. 梳理各個二方/三方庫,找到可以延遲載入的庫,做延遲載入處理,比如放到首頁控制器的viewDidAppear方法裡。

  2. 梳理業務邏輯,把可以延遲執行的邏輯,做延遲執行處理。比如檢查新版本、註冊推送通知等邏輯。

  3. 避免複雜/多餘的計算。

  4. 避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個方法執行完,首頁控制器才能顯示,部分可以延遲建立的檢視應做延遲建立/懶載入處理。

  5. 採用效能更好的API。

  6. 首頁控制器用純程式碼方式來構建。