App 的啓動時間是體現其性能優劣的一個重要指標,啓動時間越快用戶的等待時間就越短,提高用戶體驗感,大廠應用甚至會作到「 毫秒必究 」。git
咱們將 App 啓動方式分爲:github
名稱 | 說明 |
---|---|
冷啓動 | App 啓動時,應用進程不在系統中(初次打開或程序被殺死),須要系統分配新的進程來啓動應用。 |
熱啓動 | App 退回後臺後,對應的進程還在系統中,啓動則將應用返回前臺展現。 |
本篇文章主要針對冷啓動方式進行優化分析,介紹經常使用的檢測工具及優化方法。面試
Apple 官方的《WWDC Optimizing App Startup Time》 將 iOS 應用的啓動可分爲 pre-main 階段和 main 兩個階段,最佳的啓動速度是400ms之內,最慢不得大於20s,不然會被系統進程殺死(最低配置設備)。swift
爲了更好的區分,筆者將整個啓動流程分爲三個階段, App總啓動流程 = pre-main + main函數代理(didFinishLaunchingWithOptions)+ 首屏渲染(viewDidAppear),後兩個階段都屬於 main函數
執行階段。緩存
此時對應的 App 頁面是閃屏頁的展現。性能優化
加載可執行文件網絡
加載 Mach-O
格式文件,既 App 中全部類編譯後生成的格式爲 .o
的目標文件集合。數據結構
加載動態庫架構
dyld
加載 dylib
會完成以下步驟:app
系統依賴的動態庫因爲被優化過,能夠較快的加載完成,而開發者引入的動態庫須要耗時較久。
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
的負擔。去年年末二進制重排的概念被宇宙廠帶火了起來,我的以爲噱頭大於效果,詳細內容可參考文章
啓動優化不該該是一次性的,最好的方案也不是在出現纔去解決,而應該包括:
只有在開發的前中後同時介入,才能保證 App 的出品質量,畢竟開發是前人挖坑給後人填坑的過程 😂。
Xcode自帶工具 Time Profiler 和 System Trace
Xcode11 以後新增工具 App Launch
Static Initializer Tracing
AppCode 的 Inspect Code 掃描無用代碼
fui 掃描無用的類
TinyPNG 壓縮圖片,減小 IO 操做量
020 持續更新,精品小圈子每日都有新內容,乾貨濃度極高。
結實人脈、討論技術 你想要的這裏都有!
搶先入羣,跑贏同齡人!(入羣無需任何費用)
BAT大廠面試題、獨家面試工具包,
資料免費領取,包括 數據結構、底層進階、圖形視覺、音視頻、架構設計、逆向安防、RxSwift、flutter,
做者:SimonYe
連接:https://juejin.im/post/5e950106f265da47b725eaff