App 的啓動時間是體現其性能優劣的一個重要指標,啓動時間越快用戶的等待時間就越短,提高用戶體驗感,大廠應用甚至會作到「 毫秒必究 」。git
咱們將 App 啓動方式分爲:github
名稱 | 說明 |
---|---|
冷啓動 | App 啓動時,應用進程不在系統中(初次打開或程序被殺死),須要系統分配新的進程來啓動應用。 |
熱啓動 | App 退回後臺後,對應的進程還在系統中,啓動則將應用返回前臺展現。 |
本篇文章主要針對冷啓動方式進行優化分析,介紹經常使用的檢測工具及優化方法。objective-c
Apple 官方的《WWDC Optimizing App Startup Time》 將 iOS 應用的啓動可分爲 pre-main 階段和 main 兩個階段,最佳的啓動速度是400ms之內,最慢不得大於20s,不然會被系統進程殺死(最低配置設備)。swift
爲了更好的區分,筆者將整個啓動流程分爲三個階段, App總啓動流程 = pre-main + main函數代理(didFinishLaunchingWithOptions)+ 首屏渲染(viewDidAppear),後兩個階段都屬於 main函數
執行階段。緩存
此時對應的 App 頁面是閃屏頁的展現。性能優化
加載可執行文件bash
加載 Mach-O
格式文件,既 App 中全部類編譯後生成的格式爲 .o
的目標文件集合。網絡
加載動態庫app
dyld
加載 dylib
會完成以下步驟:異步
系統依賴的動態庫因爲被優化過,能夠較快的加載完成,而開發者引入的動態庫須要耗時較久。
Rebase和Bind操做
因爲使用了ASLR
技術,在 dylib
加載過程當中,須要計算指針偏移獲得正確的資源地址。 Rebase
將鏡像讀入內存,修正鏡像內部的指針,消耗 IO
性能;Bind
查詢符號表,進行外部鏡像的綁定,須要大量 CPU
計算。
Objc setup
進行 Objc
的初始化,包括註冊 Objc
類、檢測 selector
惟一性、插入分類方法等。
Initializers
往應用的堆棧中寫入內容,包括執行 +load
方法、調用 C/C++
中的構造器函數(用 attribute((constructor))
修飾的函數)、建立非基本類型的 C++
靜態全局變量等。
從 main()
函數開始執行到 didFinishLaunchingWithOptions
方法執行結束的耗時。一般會在這個過程當中進行各類工具(監控工具、推送、定位等)初始化、權限申請、判斷版本、全局配置等。
首屏 UI
構建階段,須要 CPU
計算佈局並由 GPU
完成渲染,若是數據來源於網絡,還需進行網絡請求。
得到 main() 方法執行前的耗時比較簡單,經過 Xcode 自帶的測量方法既能夠。將 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 將環境變量 DYLD_PRINT_STATISTICS
或 DYLD_PRINT_STATISTICS_DETAILS
設爲 1
便可得到執行每項耗時:
// example
// DYLD_PRINT_STATISTICS
Total pre-main time: 383.50 milliseconds (100.0%)
dylib loading time: 254.02 milliseconds (66.2%)
rebase/binding time: 20.88 milliseconds (5.4%)
ObjC setup time: 29.33 milliseconds (7.6%)
initializer time: 79.15 milliseconds (20.6%)
slowest intializers :
libSystem.B.dylib : 8.06 milliseconds (2.1%)
libMainThreadChecker.dylib : 22.19 milliseconds (5.7%)
AFNetworking : 11.66 milliseconds (3.0%)
TestDemo : 38.19 milliseconds (9.9%)
// DYLD_PRINT_STATISTICS_DETAILS
total time: 614.71 milliseconds (100.0%)
total images loaded: 401 (380 from dyld shared cache)
total segments mapped: 77, into 1785 pages with 252 pages pre-fetched
total images loading time: 337.21 milliseconds (54.8%)
total load time in ObjC: 12.81 milliseconds (2.0%)
total debugger pause time: 307.99 milliseconds (50.1%)
total dtrace DOF registration time: 0.07 milliseconds (0.0%)
total rebase fixups: 152,438
total rebase fixups time: 2.23 milliseconds (0.3%)
total binding fixups: 496,288
total binding fixups time: 218.03 milliseconds (35.4%)
total weak binding fixups time: 0.75 milliseconds (0.1%)
total redo shared cached bindings time: 221.37 milliseconds (36.0%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 43.56 milliseconds (7.0%)
libSystem.B.dylib : 3.67 milliseconds (0.5%)
libBacktraceRecording.dylib : 3.41 milliseconds (0.5%)
libMainThreadChecker.dylib : 21.19 milliseconds (3.4%)
AFNetworking : 10.89 milliseconds (1.7%)
TestDemo : 2.37 milliseconds (0.3%)
total symbol trie searches: 1267474
total symbol table binary searches: 0
total images defining weak symbols: 34
total images using weak symbols: 97
複製代碼
合併動態庫,並減小使用 Embedded Framework
,即非系統建立的動態 Framework,若是對包體積要求不嚴格還可使用靜態庫代替。
刪除無用代碼(未使用的靜態變量、類和方法等)並抽取重複代碼。
避免在 +load
執行方法,使用 +initialize
代替。
避免使用 attribute((constructor))
,可將要實現的內容放在初始化方法中配合 dispatch_once
使用。
減小非基本類型的 C++ 靜態全局變量的個數。(由於這類全局變量一般是類或者結構體,若是在構造函數中有繁重的工做,就會拖慢啓動速度)
手動插入代碼計算耗時
在 man()
函數開始執行時就開始時間:
CFAbsoluteTime StartTime; // 記錄全局變量
int main(int argc, char * argv[]) {
@autoreleasepool {
StartTime = CFAbsoluteTimeGetCurrent();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製代碼
再在 didFinishLaunchingWithOptions
返回以前獲取結束時間,二者的差值即爲該階段的耗時:
extern CFAbsoluteTime startTime; // 申明全局變量
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//...
//...
double launchTime = (CFAbsoluteTimeGetCurrent() - startTime);
return YES;
}
複製代碼
經過這種手動埋點的方式也能夠對每一個函數進行埋點獲取耗時,但當函數多時也須要不小的工做量,且後續上線還須要移除代碼,不可複用。
Time Profiler
Xcode
自帶的工具,原理是定時抓取線程的堆棧信息,經過統計比較時間間隔之間的堆棧狀態,計算一段時間內各個方法的近似耗時。精確度取決於設置的定時間隔。
經過 Xcode → Open Developer Tool → Instruments → Time Profiler 打開工具,注意,需將工程中 Debug Information Format 的 Debug 值改成 DWARF with dSYM File,不然只能看到一堆線程沒法定位到函數。
經過雙擊具體函數能夠跳轉到對應代碼處,另外能夠將 Call Tree 的 Seperate by Thread
和 Hide System Libraries
勾選上,方便查看。
正常Time Profiler是每1ms採樣一次, 默認只採集全部在運行線程的調用棧,最後以統計學的方式彙總。因此會沒法統計到耗時太短的函數和休眠的線程,好比下圖中的5次採樣中,method3都沒有采樣到,因此最後聚合到的棧裏就看不到method3。
咱們能夠將 File -> Recording Options 中的配置調高,便可獲取更精確的調用棧。
System Trace
有時候當主線程被其餘線程阻塞時,沒法經過 Time Profiler
一眼看出,咱們還可使用 System Trace
,例如咱們故意在 dyld
連接動態庫後的回調裏休眠10ms:
static void add(const struct mach_header* header, intptr_t imp) {
usleep(10000);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_dyld_register_func_for_add_image(add);
});
....
}
複製代碼
能夠看到整個記錄過程耗時7s,但 Time Profiler
上只顯示了1.17s,且看到啓動後有一段時間是空白的。這時經過 System Trace
查看各個線程的具體狀態。
能夠看到主線程有段時間被阻塞住了,存在一個互斥鎖,切換到 Events:Thread State
觀察阻塞的下一條指令,發現0x5d39c
執行完成釋放鎖後,主線程纔開始執行。
接着咱們觀察 0x5d39c
線程,發如今主線程阻塞的這段時間,該線程執行了屢次10ms的 sleep
操做,到此就找到了主線程被子線程阻塞致使啓動緩慢的緣由。
從此,當咱們想更清楚的看到各個線程之間的調度就可使用 System Trace
,但仍是建議優先使用 Time Profiler
,使用簡單易懂,排查問題效率更高。
App Launch
Xcode11 以後新出的工具,功能至關於 Time Profiler 和 System Trace 的整合。
Hook objc_msgSend
能夠對 objc_msgSend 進行 Hook 獲取每一個函數的具體耗時,優化在啓動階段耗時多的函數或將其置後調用。實現方法可查看筆者以前的文章 經過objc_msgSend實現iOS方法耗時監控。
記錄首屏 viewDidLoad
開始時間和viewDidAppear
開始時間,二者的差值即爲整個首屏渲染耗時,若是要得到具體每一個步驟耗時,則可同main函數代理階段使用 Time Profiler
或 Hook objc_msgSend
。
xib/Storyboard
,避免佈局轉換耗時。CPU
計算時間。GPU
的負擔。去年年末二進制重排的概念被宇宙廠帶火了起來,我的以爲噱頭大於效果,詳細內容可參考筆者的文章 iOS啓動優化之二進制重排 。
啓動優化不該該是一次性的,最好的方案也不是在出現纔去解決,而應該包括:
只有在開發的前中後同時介入,才能保證 App 的出品質量,畢竟開發是前人挖坑給後人填坑的過程 😂。
Xcode自帶工具 Time Profiler 和 System Trace
Xcode11 以後新增工具 App Launch
Static Initializer Tracing
AppCode 的 Inspect Code 掃描無用代碼
fui 掃描無用的類
TinyPNG 壓縮圖片,減小 IO 操做量
今年計劃完成10個優秀第三方源碼解讀,會陸續提交到 iOS-Framework-Analysis ,歡迎 star 項目陪伴筆者一塊兒提升進步,如有什麼不足之處,敬請告知 🏆。