iOS 啟動優(yōu)化②之二進(jìn)制重排

虛擬內(nèi)存

????在了解二進(jìn)制重排之前,我們先了解虛擬內(nèi)存,詳細(xì)的可以查看iOS 系統(tǒng)是怎么管理內(nèi)存的
????電腦中所運行的程序均需經(jīng)由內(nèi)存執(zhí)行,若執(zhí)行的程序占用內(nèi)存很大或很多,則會導(dǎo)致內(nèi)存消耗殆盡。為解決該問題,Windows 中運用了虛擬內(nèi)存技術(shù),即勻出一部分硬盤空間來充當(dāng)內(nèi)存使用。主要用于解決當(dāng)多個進(jìn)程同時存在時,對物理內(nèi)存的管理。提高了CPU的利用率,使多個進(jìn)程可以同時、按需加載。所以,虛擬內(nèi)存其本質(zhì)就是一張?zhí)摂M地址和物理地址對應(yīng)關(guān)系的映射表

Page Fault

????在虛擬內(nèi)存部分,當(dāng)進(jìn)程訪問一個虛擬內(nèi)存page,而對應(yīng)的物理內(nèi)存不存在時,會觸發(fā)缺頁中斷(Page Fault),因此阻塞進(jìn)程。此時就需要先加載數(shù)據(jù)到物理內(nèi)存,然后再繼續(xù)訪問。這個對性能是有一定影響的。基于Page Fault,App在冷啟動過程中,會有大量的類、分類、三方等需要加載和執(zhí)行,此時的產(chǎn)生的 Page Fault 所帶來的的耗時是很大的。

二進(jìn)制重排原理

????編譯器在生成二進(jìn)制代碼的時候,默認(rèn)按照鏈接的 Object File(.o) 順序(也就是 Targets->Build Phases->Compile Sources 中的文件順序)寫文件,按照 Object File 內(nèi)部的函數(shù)順序?qū)懞瘮?shù)。

靜態(tài)庫文件.a就是一組.o文件的ar包,可以用ar -t查看.a包含的所有.o。

????如下圖,page1 和 page2,其中 method1method3 啟動時候需要調(diào)用,為了執(zhí)行對應(yīng)的代碼,系統(tǒng)必須進(jìn)行兩次Page Fault

默認(rèn)

????但如果把 method1method3 排布到一起,那么只需要一個 Page Fault 即可,這就是二進(jìn)制文件重排的核心原理: 將所有啟動時刻需要調(diào)用的方法排列在一起
重排

獲取啟動階段的 page fault 次數(shù)

通過 System Trace 拿到某個時間段的 page fault 次數(shù)

????日常開發(fā)中性能分析是用最多的工具無疑是Time Profiler,但Time Profiler是基于采樣的,并且只能統(tǒng)計線程實際在運行的時間,而發(fā)生Page Fault的時候線程是被blocked,所以我們需要用一個不常用但功能卻很強(qiáng)大的工具:System Trace

Product-profile-SystemTrace

????選中主線程,在 VM Activity 中的 File Backed Page In 次數(shù)就是 Page Fault 次數(shù),并且雙擊還能按時序看到引起 Page Fault 的堆棧:
查看 Main Thread 中的File Backed Page In

通過 LinkMap 拿到當(dāng)前二進(jìn)制的函數(shù)布局

????LinkMap 是 iOS 編譯過程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要將 Xcode 的 Build Settings -> Write Link Map File 設(shè)置為 YES
linkmap主要包括三大部分:

  • Object Files 生成二進(jìn)制用到的 link 單元的路徑和文件編號

  • Sections 記錄 Mach-O 每個 Segment/section 的地址范圍

  • Symbols 按順序記錄每個符號的地址范圍

????通過 Link map 文件的查看,我們可以看到 在 Symbols 中有著二進(jìn)制的函數(shù)布局

Link map 的 Symbols

通過 Order File 讓鏈接器按照指定順序生成 Mach-O

????Xcode 使用的鏈接器件是 ld ,ld 有一個不常用的參數(shù) -order_file,我們可以通過在 Build Settings -> Order File 配置一個后綴為 .order 的文件路徑。 通過 Order File 我們可以更改函數(shù)和數(shù)據(jù)布局的順序。當(dāng)然,我們最好不要在調(diào)試或開發(fā)配置中指定 Order File,因為這會使鏈接的二進(jìn)制文件對調(diào)試器的可讀性降低。僅在發(fā)布時使用 Order File

不需要擔(dān)心 Order File 中的符號是不存在的,因為 ld 會忽略這些符號

Build Settings -> Order File

那么,如何編寫自己的 .order 文件呢?可以參考下面的示例:

test //函數(shù)
[ViewController orderTest]//方法

當(dāng)我們編寫完 Order File 文件后,重新編譯工程就可以在 LinkMap 文件中查看到已經(jīng)調(diào)整后的二進(jìn)制函數(shù)布局

Clang 插樁

還剩下最后一個,也是最核心的一個問題,獲取啟動時候用到的函數(shù)符號。LLVM 內(nèi)置了一個簡單的代碼覆蓋率檢測 SanitizerCoverage。它在函數(shù)級、基本塊級和邊緣級插入對用戶定義函數(shù)的調(diào)用。提供了這些回調(diào)的默認(rèn)實現(xiàn),并實現(xiàn)了簡單的覆蓋率報告和可視化。

配置 SanitizerCoverage

  • 工程 Target 配置
    Targets->Build Settings -> Other C Flags 中添加 -fsanitize-coverage=func,trace-pc-guard

    -fsanitize-coverage=func,trace-pc-guard

  • Podfile 配置

    post_install do |installer|
      installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
          config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard'
        end
      end
    end
    

實現(xiàn) SanitizerCoverage 的方法

實現(xiàn) __sanitizer_cov_trace_pc_guard_init__sanitizer_cov_trace_pc_guard 方法

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);

}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
   if (!*guard) return;  // Duplicate the guard check.
   void *PC = __builtin_return_address(0);
   printf("guard: %p %x PC\n", guard, *guard);
}

獲取函數(shù)符號

使用-fsanitize-coverage=func,trace-pc-guard 編譯器將會在每個函數(shù)的邊緣插入 __sanitizer_cov_trace_pc_guard 函數(shù)。 __builtin_return_address(0) 返回當(dāng)前函數(shù)返回地址也就是當(dāng)前函數(shù)的調(diào)用者。我們通過 dladdr()函數(shù)根據(jù) PC 指針可以獲取到其相關(guān)信息。

#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    printf("sname:%s \nsaddr:%p \n",info.dli_sname,info.dli_saddr);
}

dli_sname

可以查看到 info.dli_sname 是我們想要的函數(shù)符號信息,我們可以將 App 啟動過程中將這些信息去重存儲到數(shù)組中,啟動完成后在沙盒中新建 .order 文件,并將數(shù)據(jù)寫入。

最后,將 .order 文件從沙盒中取出,配置到工程 Order File

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容