1. 程式人生 > Android開發 >iOS基於二進位制重排啟動優化

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,實現二進位制重排