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

App啟動速度優化

應用啟動流程

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

pre-main階段

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

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

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

main()階段

2.1. dyld呼叫main()

2.2. 呼叫UIApplicationMain()

2.3. 呼叫applicationWillFinishLaunching

2.4. 呼叫didFinishLaunchingWithOptions

啟動耗時的測量

在進行優化之前,我們首先應該能測量各階段的耗時。

1. pre-main階段

對於pre-main階段,Apple提供了一種測量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環境變數DYLD_PRINT_STATISTICS 設為1 。之後控制檯會輸出類似內容:

1

2

3

4

5

6

7

8

Total pre-main time: 228.41 milliseconds (100.0%)

dylib loading time:  

82.35 milliseconds (36.0%)

rebase/binding time:   6.12 milliseconds (2.6%)

ObjC setup time:   7.82 milliseconds (3.4%)

initializer time: 132.02 milliseconds (57.8%)

slowest intializers :

libSystem.B.dylib : 122.07 milliseconds (53.4%)

CoreFoundation :   5.59 milliseconds (2.4%)

這樣我們可以清晰的看到每個耗時了。

2.main()階段

mian()階段主要是測量mian()函式開始執行到didFinishLaunchingWithOptions執行結束的時間,我們直接插入程式碼就可以了。

1

2

3

CFAbsoluteTime StartTime;

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

StartTime = CFAbsoluteTimeGetCurrent();

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

1

extern CFAbsoluteTime StartTime;

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

1

double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);

改善啟動時間

pre-main階段

在這一階段,我們能做的主要是優化dylib

載入 Dylib

之前提到過載入系統的 dylib 很快,因為有優化。但載入內嵌(embedded)的 dylib 檔案很佔時間,所以儘可能把多個內嵌 dylib 合併成一個來載入,或者使用 static archive。

使用 dlopen() 來在執行時懶載入是不建議的,這麼做可能會帶來一些問題,並且總的開銷更大。

Rebase/Binding

之前提過 Rebaing 消耗了大量時間在 I/O 上,而在之後的 Binding 就不怎麼需要 I/O 了,而是將時間耗費在計算上。所以這兩個步驟的耗時是混在一起的。

之前說過可以從檢視 __DATA 段中需要修正(fix-up)的指標,所以減少指標數量才會減少這部分工作的耗時。對於 ObjC 來說就是減少 Class,selector 和 category 這些元資料的數量。從編碼原則和設計模式之類的理論都會鼓勵大家多寫精緻短小的類和方法,並將每部分方法獨立出一個類別,其實這會增加啟動時間。對於 C++ 來說需要減少虛方法,因為虛方法會建立 vtable,這也會在 __DATA 段中建立結構。雖然 C++ 虛方法對啟動耗時的增加要比 ObjC 元資料要少,但依然不可忽視。

Objc setup

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

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

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.首頁控制器用純程式碼方式來構建。

總結

總結起來,好像啟動速度優化就一句話:讓系統在啟動期間少做一些事。當然我們得先清楚工程裡做的哪些事是在啟動期間做的、對啟動速度的影響有多大,然後case by case地分析工程程式碼,通過放到子執行緒、延遲載入、懶載入等方式讓系統在啟動期間更輕鬆些。

引用