天天看點

手淘架構組最新實踐 | iOS基于靜态庫插樁的⼆進制重排啟動優化

手淘架構組最新實踐 | iOS基于靜态庫插樁的⼆進制重排啟動優化

作者|謝俊逸(極目)

出品|阿裡巴巴新零售淘系技術部

本文知識點提煉:

1、APP 啟動時 PageFault 的性能分析

2、靜态庫插樁重排方案的技術原理

背景

近期抖音和Facebook分享了自己通過二進制重排優化啟動時間的方案,手淘iOS架構團隊也對二進制重排進行了研究,由于手淘工程子產品已經二進制化,是以實作了一套基于靜态庫插樁的重排方案。

APP 啟動 和 PageFault

當我們向作業系統申請記憶體時,作業系統并不是直接配置設定給我們實體記憶體,而是隻标記目前程序擁有該段記憶體,當真正使用這段段記憶體時才會配置設定。這種延遲配置設定實體記憶體的方式就通過page fault機制來實作的。當我們通路一個記憶體位址時,如果該位址非法,或者我們對其沒有通路權限,或者該位址對應的實體記憶體還未配置設定,cpu都會生成一個page fault,進而執行作業系統的page fault handler。如果是因為還未配置設定實體記憶體,作業系統會立即配置設定實體記憶體給目前程序,然後重試産生這個page fault的記憶體通路指令。

手淘架構組最新實踐 | iOS基于靜态庫插樁的⼆進制重排啟動優化

App在啟動時,需要執行各種函數,我們需要讀取TEXT段代碼到實體記憶體中,這個過程會發生缺⻚中斷,由于啟動時所需要執行的代碼分布在TEXT段的各個部分,會讀取很多⻚面,導緻啟動時Page Fault 數量非常多。與直接通路實體記憶體不同,page fault過程大部分是由軟體完成的,消耗時間比較久,是以是影響啟動性能的一個關鍵名額。

例如下圖中,手淘啟動時首先的調用的幾個方法 會分布在虛拟記憶體的各個⻚面中, 執行這些方法時,需要從讀取到實體内容中,就會産生多次page fault。

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

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

手淘架構組最新實踐 | iOS基于靜态庫插樁的⼆進制重排啟動優化

重排方案

如何擷取方法的執行順序

為了生成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]
; 0x2f18@PAGEOFF, _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% https://mp.weixin.qq.com/s/Drmmx5JtjG3UtTFksL6Q8Q 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

We are hiring

淘寶基礎平台團隊正在進行社招招聘,崗位有iOS Android用戶端開發工程師、Java研發工程師、C/C++研發工程師、前端開發工程師、算法工程師,歡迎投遞履歷至📮:[email protected]

如果你想更詳細了解淘寶基礎平台團隊,點選下方“閱讀原文”觀看團隊介紹視訊

更多淘寶基礎平台團隊的技術分享,可關注淘系技術微信公衆号AlibabaMTT