Categories
程式開發

手淘架構組最新實踐:iOS基於靜態庫插樁的二進制重排啟動優化


背景

近期抖音和Facebook 分享了自己通過二進制重排優化啟動時間的方案,手淘iOS 架構團隊也對二進制重排進行了研究,由於手淘工程模塊已經二進制化,因此實現了一套基於靜態庫插樁的重排方案。

APP 啟動 和 PageFault

當我們向操作系統申請內存時,操作系統並不是直接分配給我們物理內存,而是只標記當前進程擁有該段內存,當真正使用這段內存時才會分配。這種延遲分配物理內存的方式就通過 page fault 機制來實現的。當我們訪問一個內存地址時,如果該地址非法,或者我們對其沒有訪問權限,或者該地址對應的物理內存還未分配, cpu 都會生成一個 page fault ,進而執行操作系統的 page fault handler 。如果是因為還未分配物理內存,操作系統會立即分配物理內存給當前進程,然後重試產生這個 page fault 的內存訪問指令。

手淘架構組最新實踐:iOS基於靜態庫插樁的二進制重排啟動優化 1

App 在啟動時,需要執行各種函數,我們需要讀取TEXT 段代碼到物理內存中,這個過程會發生缺⻚中斷,由於啟動時所需要執行的代碼分佈在TEXT 段的各個部分,會讀取很多⻚面,導致啟動時Page Fault 數量非常多。與直接訪問物理內存不同, page fault 過程大部分是由軟件完成的,消耗時間比較久,所以是影響啟動性能的一個關鍵指標。

例如下圖中,手淘啟動時首先的調用的幾個方法 會分佈在虛擬內存的各個⻚面中, 執行這些方法時,需要從讀取到物理內容中,就會產生多次 page fault 。

如果能將啟動階段需要的讀取代碼集中排布,將這些方法全都放到相鄰的區域中,我們讀取這些方法可能就只需要極少的 page fault 次數。可以減少不必要的 page fault 時間。達到優化啟動時間的效果。

重排前後的函數在頁面的佈局對比:

手淘架構組最新實踐:iOS基於靜態庫插樁的二進制重排啟動優化 2

重排方案

如何獲取方法的執行順序

為了生成 order_file , 我們需要確定應用啟動時方法的執行順序。之前抖音和 facebook 都分享過自己的方案,在實際操作的過程中,我們發現抖音和 facebook 的方案並不適用於手淘。

抖音通過靜態掃描和運行時 Trace 等方法確定 order_file,該方案無法覆蓋 initialize、block 和 C++ 通過寄存器的間接函數調用靜態掃描不出來調用。

facebook 分享過通過 llvm 插樁的確定 order_file 的方案,需要使用源碼重新打包。由於手淘幾乎全是已經編譯好的二進制模塊,在手淘使用該方案不現實。

只能想其他辦法…

手淘之前已經做過 pod 預編譯,我和師兄念紀想到了是否可以通過在彙編層面對 pod 編譯後的靜態庫進行插樁。在啟動時,插樁後的方法都會調用記錄方法,從而獲得啟動方法的執行順序。在參考了離青對彙編插樁的研究後,確定了靜態庫插樁的實現方案。

靜態庫插樁

我們編譯過的靜態庫由 .o 文件組成,我們可以對 .o 中的函數代碼進行修改,在每個函數的開頭插入調用我們指定記錄函數的指令。

舉個例子:

插入前 -[MyApp window]: 的彙編代碼

-[MyApp window]:
0000000000002d88 adrp x8, #0x
0000000000002d8c ldrsw x8, [x8, #0xf18]
; [email protected], _OBJC_IVAR_$_MyApp._window
0000000000002d90 ldr x0, [x0, x8]
0000000000002d94 ret

插入後的 彙編代碼,可以看到 增加了跳轉到 _record_method 的指令,並且補上了 prologue 和 epilogue 。

-[MyApp window]:
0000000000002ebc stp x29, x30, [sp, #-0x10]!
0000000000002ec0 mov x29, sp
0000000000002ec4 bl _record_method
0000000000002ec8 ldp x29, x30, [sp], #0x
0000000000002ecc adrp x8, #0x
0000000000002ed0 ldrsw x8, [x8, #0xc0]
0000000000002ed4 ldr x0, [x0, x8]
0000000000002ed8 ret

生成 order file

linkmap 記錄了連接過程中的相關信息。其中包含鏈接用到的 symbol 相關的信息。通過 pc address 減去 slide 得到的地址,我們可以在 linkmap 中找到對應的 symbol .

address = pc - slide. // 因为ASLR, APP 可执行文件随机载入的原因,需要处理一下偏移
量。

我們需要將之前記錄的地址轉換成對應的符號,為了真實還原線上的執行環境,我們只是在 app 中簡單地的記錄了 pc 地址 和 Image 的偏移量。通過解析 linkmap ,獲取函數的地址區間, 得到距離 address 最近的 symbol ,生成 order_file 。

linkmap 文件:

# Symbols:
# Address Size File Name
0x100001630 0x00000039 [ 2] -[ViewController viewDidLoad]
0x100001670 0x00000092 [ 3] _main
0x100001710 0x00000080 [ 4] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100001790 0x00000040 [ 4] -[AppDelegate applicationWillResignActive:]
0x1000017D0 0x00000040 [ 4] -[AppDelegate applicationDidEnterBackground:]
0x100001810 0x00000040 [ 4] -[AppDelegate applicationWillEnterForeground:]
0x100001850 0x00000040 [ 4] -[AppDelegate applicationDidBecomeActive:]
0x100001890 0x00000040 [ 4] -[AppDelegate applicationWillTerminate:]

更改符號的排列順序

默認情況下, ld 鏈接器會按照鏈接的順序將各個 .o 文件的數據重新佈局生成可執行文件。 ld 鏈接器提供 -order-file 選項操控數據排列的順序。在 Xcode 中可以通過 Order File 選項指定符號排序文件。

//Order file 内容例子:
+[xxxxx1 load]
+[xxxxx2 swizzleResumeAndSuspendMethodForClass:]
+[xxxxx3 load]
+[xxxxx4 initialize]___
+[xxxxx5 initialize]_block_invoke
+[xxxxx6 initialize]___
+[xxxxx7 initialize]_block_invoke
...

優化效果

通過精準的啟動函數重排,最後重排效果還是很可觀的,在 iPhone6 上優化了400ms 的啟動時間。

參考

感謝抖音團隊和 Facebook 團隊提供優化新思路

抖音研發實踐:基於二進製文件重排的解決方案 APP啟動速度提升超15%

Improving iOS Startup Performance with Binary Layout Optimizations
https://atscaleconference.com/videos/performance-scale-improving-ios-startup-performance-with-binary-layout-optimizations/

Linux下Page Fault的處理流程 https://cloud.tencent.com/developer/article/1459526

本文轉載自公眾號淘系技術(ID:AlibabaMTT)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650405083&idx=1&sn=f52e8dcf03faafcf910d58add9904550&chksm=839530c3b4e2b9d5cab0e84da1a0d6c210ed7fb2fc4a66d01213fdfb2fcffbbe332c87b8eb76&scene=27#wechat_redirect