iOS基於二進位制重排啟動優化
一、重排原理
當我們向作業系統申請記憶體時,作業系統並不是直接分配給我們實體記憶體,而是隻標記當前程式擁有該段記憶體,當真正使用這段記憶體時才會分配。這種延遲分配實體記憶體的方式就通過 page fault 機制來實現的。
1.page fault產生原因
當我們訪問一個記憶體地址時,如果該地址非法,或者我們對其沒有訪問許可權,或者該地址對應的實體記憶體還未分配, cpu 都會生成一個 page fault ,進而執行作業系統的 page fault handler 。如果是因為還未分配實體記憶體,作業系統會立即分配實體記憶體給當前程式,然後重試產生這個 page fault 的記憶體訪問指令。
2.二進位制重排原理
App啟動時首先的呼叫的幾個方法,會分佈在虛擬記憶體的各個⻚面中,執行這些方法時,需要從讀取到物理內容中,就會產生多次 page fault 。
如果能將啟動階段需要的讀取程式碼集中排布,將這些方法全都放到相鄰的區域中,我們讀取這些方法可能就只需要極少的 page fault 次數。可以減少不必要的 page fault 時間。達到優化啟動時間的效果。
重排前後的函式在頁面的佈局對比
根據經驗優化一個Page Fault,啟動速度提升0.6~0.8ms,AppStore分發應用還要做簽名認證,所以耗時相對更長二、實現
1、System Trace除錯
- 首先我們開啟專案command + i,開啟Instruments除錯工具,選擇System Trace
- 選擇真機,選擇工程,點選開始後,當首個頁面載入出來點選停止,這裡我們搜尋Main thread,選擇我們的app,然後點選Main thread ,再到下面選擇Main Thread --> Virtual Memory(虛擬記憶體)
- 這裡面File Backed Page In就是page fault的次數
- 當我們把APP殺死後裡面再啟動,結果發現File Backed Page In這個值變得很小,說明APP就算殺死後,在啟動不是冷啟動,還是有一部資料在系統的快取中
- 如何才是真正的冷啟動呢,我們可以把APP殺掉後啟動多個手機裡面的APP,然後再啟動APP,發現File Backed Page In又變得很大
- 二進位制重排是在連結階段生成的,重排之後生成可執行檔案,所以我們只能在編譯階段來優化,而無法對已生成的ipa進行優化
2.二進位制重排
我們可以在XCode配置二進位制重排,首先我們要確定符號的順序,才能知道怎麼重排,XCode使用的連結器叫做ld,ld有個引數叫order_file,我們可以將檔案的路徑告訴XCode,在order_file檔案中把符號的順序寫進去,XCode編譯的時候就會按照檔案中的符號順序打包成二進位制可執行檔案。
我們可以在蘋果的objc4-750原始碼中找到這種檔案
開啟後是下面這種格式:裡面全是函式符號,我們開啟專案,在build setting 裡面搜尋order file,發現這裡面指定了order的檔案路徑,因為一旦在這裡指定了order file的路徑,XCode就會在編譯的時候按照檔案裡面寫進去的順序
我們現在寫一個Demo,AppDelegate新增如下方法
+ (void)test111 {
NSLog(@"test111");
}
+ (void)test222 {
NSLog(@"test222");
}
+ (void)test333 {
NSLog(@"test333");
}
複製程式碼
然後編譯,如何檢視整個專案的符號順序呢,我們到Build Settings搜尋Link Map,Link Map就是我們連結的符號表,我們把它改成YES,這樣編譯的時候就會把連結的符號表給我們寫出來
command + R我們執行下,然後在Products裡面的.app檔案,在我們Intermediates.noindex-->專案名.build--->Debug-iphoneos-->專案名.build--->專案名-LinkMap-normal-x86_64.txt,這個檔案裡面就有連結的符號順序表
我們在專案中用touch建立test.order檔案,修改方法順序
然後在Build setting裡面搜下order file,然後在後面將該檔案地址新增進去
這樣Xcode在編譯時候就會按照order檔案中的符號順序連結程式碼了,我們編譯一下,再看一下LinkMap-normal-x86_64.txt檔案
我們發現是按照order的符號順序來的,而且如果order裡面寫了專案中不存在的方法符號,XCode會自動過濾掉,不存在影響
我們二進位制重排並非只是修改符號地址,而是利用符號順序,重新排列整個程式碼在檔案的偏移地址,將啟動需要載入的方法地址放到前面記憶體頁中,以此達到減少 page fault 的次數從而實現時間上的優化,一定要清楚這一點
三、獲取APP啟動時候呼叫的所有方法
第一種 通過靜態掃描和執行時 Trace 等方法確定 order_file
參考抖音研發實踐:基於二進位制檔案重排的解決方案 APP啟動速度提升超15%
流程如下:
1.設定條件觸發流程
2.工程注入Trace動態庫,選擇release模式編譯出.app/linkmap/中間產物
3.執行一次App到啟動結束,Trace動態庫會在沙盒生成Trace log
4.以Trace Log,中間產物和linkmap作為輸入,執行指令碼解析出order_file
缺點
目前不足的是,該方案無法覆蓋 initialize、block 和 C++ 通過暫存器的間接函式呼叫靜態掃描不出來呼叫,因為是用fishHook去hook 系統的 objc_msgSend這個函式,因為oc的方法都是通過傳送訊息的形式,但是這個函式引數是可變的引數,所以只能通過彙編形式hook,但是這種情況initialize和block以及直接呼叫函式方式hook不到
第二種 基於llvm插樁的方案
簡單來說 SanitizerCoverage 是 Clang 內建的一個程式碼覆蓋工具。它把一系列以 __sanitizer_cov_trace_pc_
為字首的函式呼叫插入到使用者定義的函式裡,藉此實現了全域性 AOP 的大殺器。其覆蓋之廣,包含 Swift/Objective-C/C/C++ 等語言,Method/Function/Block 全支援。
開啟 SanitizerCoverage 的方法是:在 build settings 裡的 “Other C Flags” 中新增 -fsanitize-coverage=func,trace-pc-guard
。如果含有 Swift 程式碼的話,還需要在 “Other Swift Flags” 中加入 -sanitize-coverage=func
和 -sanitize=undefined
。所有連結到 App 中的二進位制都需要開啟 SanitizerCoverage,這樣才能完全覆蓋到所有呼叫。
騰訊大神寫了個工具 AppOrderFiles。CocoaPods 接入,程式啟動完成函式一行呼叫,生成 Order File。全在 GitHub 裡了:github.com/yulingtianx…
AppOrderFiles(^(NSString *orderFilePath) {
NSLog(@"OrderFilePath:%@",orderFilePath);
});
複製程式碼
也可以參考facebook LLVM插樁視訊
缺點
通過 llvm 插樁的確定 order_file 的方案,需要使用原始碼重新打包。如果專案全是已經編譯好的二進位制模組,使用該方案效果不佳
第三種 手機淘寶團隊靜態庫插樁方案
通過在彙編層面對 pod 編譯後的靜態庫進行插樁。在啟動時,插樁後的方法都會呼叫記錄方法,從而獲得啟動方法的執行順序。我們編譯過的靜態庫由 .o 檔案組成,我們可以對 .o 中的函式程式碼進行修改,在每個函式的開頭插入呼叫我們指定記錄函式的指令。
參考資料 手淘架構組最新實踐 | iOS基於靜態庫插樁的⼆進位制重排啟動優化
總結
本文主要介紹 page fault產生原因,在啟動階段通過 order file 機制實現二進位制重排 ,減少執行 page fault 的次數,加快應用的啟動速度。生成order file主流有兩種方式,一種是通過靜態掃描和執行時 Trace 等方法確定 order file,另外一種是基於llvm插樁的方案,然後工程配置order file,實現二進位制重排