APP啓動優化
App啓動過程
iOS應用的啓動可以分爲pre-main階段和main()階段,其中系統做的事情一次是:
無論對於系統的動彈鏈接還是對於APP本身的可執行文件而言,它們都算是image(鏡像),而每個APP都是以image(鏡像)爲單位進行加載的
什麼是image(鏡像)
- Executable:應用的主要二進制(比如.o文件)
- Dylib:動態鏈接庫(dynamic library,又稱DSO或DLL)
- Bundle:資源文件,不能被鏈接的dylib,只能在運行時使用dlopen()加載
pre-main階段
- 加載應用的可只能文件(自身APP所有的.o文件的集合)
- 加載動態鏈接(dynamic loader,是一個專門用來加載動態鏈接庫的庫)
- dyld遞歸加載應用所有依賴的動態鏈接庫dylib
main()階段
- dyld調用main()
- 調用UIApplicationMain()
- 調用applicationWillFinishLaunching
- 調用didFinishLaunchingWithOptions
pre-main階段的過程和優化項
對於pre-main階段的耗時優化,我們需要知道dyld加載的過程,蘋果在2016年WWDC上介紹,dyld的加載過程主要分爲4步:
1、 Load dylibs
分析應用依賴的dylib(xcode7以後.dylib已改爲名.tbd),找到其mach-o文件,打開和讀取這些文件並驗證其有效性,接着會找到代碼簽名註冊到內核,最後對dylib的每一segment調用mmap()
一般情況下,iOS應用會加載100-400個dylibs,其中大部分是系統庫,這部分dylib的加載系統已經做了優化。
所以,依賴的dylib越少越好,我們可以做的優化有:
- 儘量不要使用內嵌的dylib,加載內嵌dylib的性能開銷比較大
- 合併已有的dylib和使用靜態庫,減少dylib的使用個數
- 依賴加載dylib,但是要注意dlopen()可能造成一些問題,且實際上依賴加載做的工作更多
Rebase/Bind
在dylib加載的過程中,系統爲了安全考慮引用ASLR(address space layout randomization)技術和簽名。由於ASLR 的存在,鏡像會隨機的地址上加載,和之前的指針指向的地址,prefered_address會有一個偏差,dyld需要修正這個偏差,來指向正確的地址。
Rebase在前,Bind在後,Rebase做的是將鏡像讀入內存,修正鏡像內部的指針,性能消耗,主要實在IO,Bind做的是查詢符號表,設置指向鏡像外部的指針,性能消耗主要在CPU
所以指針數量越少越好,
- 減少objc類,方法selector、分類category的數量
- 減少c++虛函數的數量
- 使用swift struct(內部做了優化,符號數量更少)
objc setup
大部分objc初始化工作已經在rebase/bind 階段左外,這一步dyld會註冊所有生命過的objc,將分類插入到類方法列表中,再堅持每個selection的唯一性
這一步沒什麼可優化的,rebase/bind階段優化好了,這一步耗時也會減少
Initializers
dyld開始運行程序的初始化函數,調用每個objc類和分類的+load方法,調用c/c++ 中的構造器,和創建非基本類型的c++靜態全局變量。initializers階段執行完後,dyld開始調用main函數
objc的load函數和C++的靜態構造函數採用由低向上的方式執行,來保證每一個執行的方法,都可以找到所有的動態庫
優化:
- 少在類的+load方法中做事情,儘量把這些事情推遲到+initialize
- 減少構造器個數,在構造函數中少做事情
- 減少C++靜態變量的個數
main()階段的優化項
這一階段的優化主要是減少didFinishLaunchingWithOptions方法裏的工作,在didFinishLaunchingWithOptions方法裏,我們會創建應用的window,指定其rootViewController,調用window的makeKeyAndVisible方法讓其可見。由於業務需要,我們會初始化各個二方/三方庫,設置系統UI風格,檢查是否需要顯示引導頁、是否需要登錄、是否有新版本等,由於歷史原因,這裏的代碼容易變得比較龐大,啓動耗時難以控制。
所以,滿足業務需要的前提下,didFinishLaunchingWithOptions在主線程裏做的事情越少越好。在這一步,我們可以做的優化有:
-
梳理各個二方/三方庫,找到可以延遲加載的庫,做延遲加載處理,比如放到首頁控制器的viewDidAppear方法裏。
-
梳理業務邏輯,把可以延遲執行的邏輯,做延遲執行處理。比如檢查新版本、註冊推送通知等邏輯。
-
避免複雜/多餘的計算。
-
採用性能更好的API。
-
避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個方法執行完,首頁控制器才能顯示,部分可以延遲創建的視圖應做延遲創建/懶加載處理。
-
首頁控制器用純代碼方式來構建。
啓動耗時的測量
pre-main階段:
xcode9以後APP提供了一種測試方法,在Xcode中Edit scheme-> Run -> Auguments
設置環境變量:DYLD_PRINT_STATISTICS爲1
main()階段測量
對於main()階段,主要是測量main()函數開始執行到didFinishLaunchingWithOptions執行結束的耗時,就需要自己插入代碼到工程中了。先在main()函數裏用變量StartTime記錄當前時間
CFAbsoluteTime StartTime;
int main(int argc, char *argv[]){
StartTime = CFAbsoluteTimeGetCurrent();
}
再在AppDelegate.m文件中用extern聲明全局變量StartTime
extern CFAbsoluteTime Startime;
最後在didFinishLaunchingWithOptions裏,再獲取一下當前時間,與StartTime的差值即是main()階段運行耗時。
double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime)