介紹 App 的啟動時間是體現其性能優劣的一個重要指標,啟動時間越快用戶的等待時間就越短,提升用戶體驗感,大廠應用甚至會做到“ 毫秒必究 ”。 我們將 App 啟動方式分為: 名稱說明 冷啟動 App 啟動時,應用進程不在系統中(初次打開或程式被殺死),需要系統分配新的進程來啟動應用。 熱啟動 Ap ...
介紹
App 的啟動時間是體現其性能優劣的一個重要指標,啟動時間越快用戶的等待時間就越短,提升用戶體驗感,大廠應用甚至會做到“ 毫秒必究 ”。
我們將 App 啟動方式分為:
名稱 | 說明 |
---|---|
冷啟動 | App 啟動時,應用進程不在系統中(初次打開或程式被殺死),需要系統分配新的進程來啟動應用。 |
熱啟動 | App 退回後臺後,對應的進程還在系統中,啟動則將應用返回前臺展示。 |
本篇文章主要針對冷啟動方式進行優化分析,介紹常用的檢測工具及優化方法。
冷啟動流程
Apple 官方的《WWDC Optimizing App Startup Time》 將 iOS 應用的啟動可分為 pre-main 階段和 main 兩個階段,最佳的啟動速度是400ms以內,最慢不得大於20s,否則會被系統進程殺死(最低配置設備)。
為了更好的區分,筆者將整個啟動流程分為三個階段, App總啟動流程 = pre-main + main函數代理(didFinishLaunchingWithOptions)+ 首屏渲染(viewDidAppear),後兩個階段都屬於 main函數
執行階段。
pre-main 執行內容
此時對應的 App 頁面是閃屏頁的展示。
-
載入可執行文件
載入
Mach-O
格式文件,既 App 中所有類編譯後生成的格式為.o
的目標文件集合。 -
載入動態庫
dyld
載入dylib
會完成如下步驟:- 分析 App 依賴的所有 dylib。
- 找到 dylib 對應的 Mach-O 文件。
- 打開、讀取這些 Mach-O 文件,並驗證其有效性。
- 在系統內核中註冊代碼簽名。
- 對 dylib 的每一個 segment 調用 mmap()。
系統依賴的動態庫由於被優化過,可以較快的載入完成,而開發者引入的動態庫需要耗時較久。
-
Rebase和Bind操作
由於使用了
ASLR
技術,在dylib
載入過程中,需要計算指針偏移得到正確的資源地址。Rebase
將鏡像讀入記憶體,修正鏡像內部的指針,消耗IO
性能;Bind
查詢符號表,進行外部鏡像的綁定,需要大量CPU
計算。 -
Objc setup
進行
Objc
的初始化,包括註冊Objc
類、檢測selector
唯一性、插入分類方法等。 -
Initializers
往應用的堆棧中寫入內容,包括執行
+load
方法、調用C/C++
中的構造器函數(用attribute((constructor))
修飾的函數)、創建非基本類型的C++
靜態全局變數等。
main函數代理執行內容
從 main()
函數開始執行到 didFinishLaunchingWithOptions
方法執行結束的耗時。通常會在這個過程中進行各種工具(監控工具、推送、定位等)初始化、許可權申請、判斷版本、全局配置等。
首屏渲染執行內容
首屏 UI
構建階段,需要 CPU
計算佈局並由 GPU
完成渲染,如果數據來源於網路,還需進行網路請求。
優化方案
pre-main階段
檢測方法
獲得 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++ 靜態全局變數的個數。(因為這類全局變數通常是類或者結構體,如果在構造函數中有繁重的工作,就會拖慢啟動速度)
main函數代理階段
檢測方法
-
手動插入代碼計算耗時
在
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方法耗時監控。
優化點
- 通過檢測工具找到耗時多的函數,拆分其功能,將優先順序低的功能延後執行。
- 梳理業務邏輯,把可以延遲執行的邏輯,做延遲執行處理。比如檢查新版本、註冊推送通知等邏輯。
- 梳理各個二方/三方庫,找到可以延遲載入的庫,做延遲載入處理,比如放到首頁控制器的viewDidAppear方法後。
首屏渲染階段
檢測方法
記錄首屏 viewDidLoad
開始時間和viewDidAppear
開始時間,兩者的差值即為整個首屏渲染耗時,如果要獲得具體每個步驟耗時,則可同main函數代理階段使用 Time Profiler
或 Hook objc_msgSend
。
優化點
- 使用簡單的廣告頁作為過渡,將首頁的計算操作及網路請求放在廣告頁展示時非同步進行。
- 涉及活動需變更頁面展示時(例如雙十一),提前下發數據緩存。
- 首頁控制器用純代碼方式來構建,而不是
xib/Storyboard
,避免佈局轉換耗時。 - 避免在主線程進行大量的計算,將與首屏無關的計算內容放在頁面展示後進行,縮短
CPU
計算時間。 - 避免使用大圖片,減少視圖數量及層級,減輕
GPU
的負擔。 - 做好網路請求介面優化(DNS 策略等),只請求與首屏相關數據。
- 本地緩存首屏數據,待渲染完成後再去請求新數據。
其它優化
二進位重排
去年年底二進位重排的概念被宇宙廠帶火了起來,個人覺得噱頭大於效果,詳細內容可參考文章
總結
啟動優化不應該是一次性的,最好的方案也不是在出現才去解決,而應該包括:
- 解決現存的問題
- 後續開發的管控
- 完整的監控體系
只有在開發的前中後同時介入,才能保證 App 的出品質量,畢竟開發是前人挖坑給後人填坑的過程