極客時間戴銘學習筆記
App啟動干了什么
一般分為冷啟動和熱啟動兩種
冷啟動指, App點擊啟動前, 進程不在系統(tǒng)里, 需要系統(tǒng)新創(chuàng)建一個進程分配給該App, 這是一個完整的啟動過程
熱啟動指, App在冷啟動后用戶將app退到后臺, 在App的進程還在系統(tǒng)里用戶重新進入?的過程
冷啟動階段
三個階段: main函數(shù)執(zhí)行前小槐、main之后台囱、首屏渲染完成后
main()執(zhí)行前
加載可執(zhí)行文件(App的.o文件的集合)
加載動態(tài)鏈接庫, 進行rebase指針調整和bind符號綁定
Objc運行時的初始化處理, 包括Objc相關類的注冊媒区、category注冊砚作、selector唯一性檢查等
初始化, 包括了執(zhí)行+load()方法拱礁、attribute((constructor))修飾的函數(shù)調用揍移、創(chuàng)建C++靜態(tài)全局變量
相應優(yōu)化方法
減少動態(tài)庫加載. 每個庫本身都有依賴關系, 蘋果公司建議使用更少的動態(tài)庫, 并且建議在使用動態(tài)庫的數(shù)量較多時, 盡量將多個動態(tài)庫進行合并. 數(shù)量上, 最多可以支持6個非系統(tǒng)動態(tài)庫合并為一個
減少加載啟動后不會去使用的類或方法
+load()方法里的內容可以放到首屏渲染完成后再執(zhí)行, 或者用+initialize()方法替換掉, 因為, 在一個+load()方法里,進行運行時方法替換操作會帶來4毫秒的消耗
控制C++全局變量的數(shù)量
main()函數(shù)執(zhí)行后
main()函數(shù)執(zhí)行后的階段, 指從main()函數(shù)執(zhí)行開始, 到appDelegate的didFinishLaunchingWithOptions方法里首屏渲染執(zhí)行完成
首頁的業(yè)務代碼都是在這個階段, 也就是首屏渲染前執(zhí)行的, 主要包括了:
首屏初始化所需配置文件的讀寫操作;
首屏列表大數(shù)據(jù)的讀取;
首屏渲染的大量計算等.
相應優(yōu)化方法
從功能上保留首屏渲染必要的初始化功能, 啟動必要的初始化功能,
其他使用時才需要初始化的可以優(yōu)化到之后的時間里執(zhí)行
首屏渲染完成后
非首屏其他業(yè)務服務模塊的初始化滑肉、監(jiān)聽的注冊、配置文件的讀取.
從函數(shù)上看, 這個階段指的是截止到didFinishLaunchingWithOptions方法作用域內執(zhí)行首屏渲染之后的所有方法執(zhí)行完成.
這個優(yōu)化的優(yōu)先級排在最后
功能級別的啟動優(yōu)化
跟前面有點重復, main()函數(shù)開始執(zhí)行后到首屏渲染完成前只處理首屏相關的業(yè)務, 其他非首屏業(yè)務的初始化七咧、監(jiān)聽注冊跃惫、配置文件讀取等放到首屏渲染完成后去做
方法級別的啟動優(yōu)化
某些方法大量調用會增加耗時.
監(jiān)控啟動速度兩種方案
- 定時抓取主線程上的方法調用堆棧, 計算一段時間里各個方法的耗時,比如Time Profiler
- 對objc_msgSend方法進行hook來掌握所有方法的執(zhí)行耗時, 通過監(jiān)聽所有的Objective-C里方法執(zhí)行來實現(xiàn)
objc_msgSend方法執(zhí)行的邏輯是: 先獲取對象對應類的信息, 再獲取方法的緩存, 根據(jù)方法的selector查找函數(shù)指針, 經(jīng)過異常錯誤處理后, 最后跳到對應函數(shù)的實現(xiàn)
使用fishhook的方法來實現(xiàn)
基于靜態(tài)庫插樁的二進制重排啟動優(yōu)化 (手淘的最新實踐)
- App 啟動時PageFault的性能分析
- 靜態(tài)庫插樁重排方案的技術原理
App啟動和PageFault
當我們向操作系統(tǒng)申請內存時, 操作系統(tǒng)并不是直接分配給我們物理內存, 而是只標記當前進程擁有該段內存, 當真正使用這段內存時才會分配. 這種延遲分配物理內存的方式就通過page fault機制來實現(xiàn)的. 當我們訪問一個內存地址時, 如果該地址非法, 或者我們對其沒有訪問權限, 或者該地址對應的物理內存還未分配, cpu都會生成一個page fault, 進而執(zhí)行操作系統(tǒng)的page fault handler. 如果是因為還未分配物理內存, 操作系統(tǒng)會立即分配物理內存給當前進程, 然后重試產(chǎn)生這個page fault的內存訪問指令App在啟動時, 需要執(zhí)行各種函數(shù), 我們需要讀取TEXT段代碼到物理內存中, 這個過程會發(fā)生缺頁中斷, 由于啟動時所需要執(zhí)行的代碼分布在TEXT段的各個部分, 會讀取很多頁面, 導致啟動時Page Fault數(shù)量非常多. 與直接訪問物理內存不同, page fault過程大部分由軟件完成, 消耗時間比較久, 所以是影響啟動性能的一個關鍵指標.
手淘啟動時首先調用的幾個方法會分布在虛擬內存的各個頁面中, 執(zhí)行這些方法時, 需要從讀取到物理內存中, 就會產(chǎn)生多次page fault.
如果能將啟動階段需要的讀取代碼集中排布, 將這些方法全都放到相鄰的區(qū)域中, 我們讀取這些方法可能就只需要極少的page fault次數(shù). 可以減少不必要的page fault時間.達到優(yōu)化啟動時間的效果.
重排前后的函數(shù)在頁面的布局對比
重排方案
如何獲取方法的執(zhí)行順序
為了生存order_file, 我們需要確定應用啟動時方法的執(zhí)行順序.
抖音通過靜態(tài)掃描和運行時Trace等方法確定order_file, 該方案無法覆蓋initialize、block和C++通過寄存器的間接函數(shù)調用
Facebook分享過通過llvm插樁的確定order_file的方案, 需要使用源碼重新打包.
靜態(tài)庫插樁
我們編譯過的靜態(tài)庫由.o文件組成, 我們可以對.o中的函數(shù)代碼進行修改, 在每個函數(shù)的開頭插入調用我們指定記錄函數(shù)的指令
生成order file
linkmap記錄了連接過程中的相關信息. 其中包含鏈接用到的symbol相關信息.通過pc address減去slide得到的地址, 我們可以在linkmap中找到對應的symbol
address = pc - slide. // 因為ASLR艾栋, APP 可執(zhí)行文件隨機載入的原因爆存,需要處理一下偏移
量。
我們需要將之前記錄的地址轉換成對應的符號, 為了真實還原線上的執(zhí)行環(huán)境, 在app中簡單的記錄了pc地址和image的偏移量. 通過解析linkmap, 獲取函數(shù)的地址區(qū)間, 得到距離address最近的symbol, 生成order_file
更改符號的排列順序
默認情況下, Id鏈接器會按照鏈接的順序將各個.o文件的數(shù)據(jù)重新布局生成可執(zhí)行文件.Id鏈接器提供-order-file選項操控數(shù)據(jù)排列的順序.在Xcode中可以通過Order File選項指定符號排序文件.