iOS 啟動優(yōu)化(二)二進制重排

App啟動分析

App啟動分析

App啟動分為 冷啟動熱啟動

  • 冷啟動:點擊 App 啟動前,它的進程不在系統(tǒng)里,需要系統(tǒng)新創(chuàng)建一個進程分配給它的情況。這是一次完整的啟動過程
  • 熱啟動:App 在冷啟動后,用戶將App 退到后臺,即在App的進程還在系統(tǒng)里的情況下,用戶重新啟動進入 App 的過程,這個過程做的事情非常少,啟動速度非常快。

因此,我們主要針對 App 冷啟動進行優(yōu)化。

一般而言,App 啟動時間,指的是從用戶點擊 App 開始,到用戶看到第一個界面之間的時間,總結來說:App 的啟動主要包括三個階段:

  1. main() 函數執(zhí)行前
  2. main() 函數執(zhí)行后
  3. 首屏渲染完成后

1、pre-main耗時檢測
通過設置環(huán)境變量來統(tǒng)計 pre-main 的耗時

選擇 `Edit Scheme` - `Arguments` - `Environment Variables`

添加 name `DYLD_PRINT_STATISTICS` value : `${DEBUG_ACTIVITY_MODE}`
啟動時間檢測日志.png

可見,在 main() 函數執(zhí)行前,系統(tǒng)主要會做下面幾件事情:

  • dylib loading:加載可執(zhí)行文件(App 的.o 文件的集合),加載動態(tài)鏈接庫
  • rebase/binding:對動態(tài)鏈接庫進行 rebase 指針調整和 bind 符號綁定;
  • Objc setup:Objc 運行時的初始化處理,包括 Objc 相關類的注冊、category 注冊、selector 唯一性檢查等;
  • initializer:初始化,包括了執(zhí)行 +load() 方法、attribute((constructor)) 修飾的函數的調用、創(chuàng)建 C++ 靜態(tài)全局變量。

相應地,這個階段對于啟動速度優(yōu)化來說,可以做的事情包括:

  • 減少動態(tài)庫加載:每個庫本身都有依賴關系,蘋果公司建議使用更少的動態(tài)庫,并且建議在使用動態(tài)庫的數量較多時,盡量將多個動態(tài)庫進行合并。數量上,蘋果公司建議最多使用 6 個非系統(tǒng)動態(tài)庫。
  • 減少加載啟動后不會去使用的類或者方法。
  • +load()方法里的內容可以放到首屏渲染完成后再執(zhí)行,或使用 +initialize()方法替換掉。因為,在一個 +load() 方法里,進行運行時方法替換操作會帶來 4 毫秒的消耗。不要小看這 4 毫秒,積少成多,執(zhí)行 +load() 方法對啟動速度的影響會越來越大。
  • 控制 C++ 全局變量的數量。
launch.png

當我們做了以上工作,對 pre-main 的時間有所優(yōu)化之后,如果還想再進行優(yōu)化,那就需要使用 LLVM 為我們提供的優(yōu)化方式:二進制重排

Page Fault

進程如果能直接訪問物理內存無疑是很不安全的,所以操作系統(tǒng)在物理內存的上又建立了一層虛擬內存。為了提高效率和方便管理,又對虛擬內存和物理內存又進行分頁(Page)。當進程訪問一個虛擬內存Page而對應的物理內存卻不存在時,會觸發(fā)一次缺頁中斷(Page Fault),分配物理內存,有需要的話會從磁盤mmap讀人數據。

通過App Store渠道分發(fā)的App,Page Fault還會進行簽名驗證,所以一次Page Fault的耗時比想象的要多


PageFault.png

LinkMap

LinkMap 是iOS編譯過程的中間產物,記錄了二進制文件的布局,需要在Xcode的Build Settings里開啟Write Link Map File:

MapLink.png

Path to Link Map File:
選中編譯后的 app,Show In Finder -- 找到build目錄 -- 具體路徑如下:
Build/Intermediates.noindex/Spirit.build/Debug-iphonesimulator/Spirit.build/Spirit-LinkMap-normal-x86_64.txt

LinkMap 主要包括三大部分:

  • Object Files 生成二進制用到的link單元的路徑和文件編號
  • Sections 記錄Mach-O每個Segment/section的地址范圍
  • Symbols 按順序記錄每個符號的地址范圍
引入文件順序.png
鏈接函數順序.png

通過MapLink就可以看到鏈接的函數的順序和引用的文件順序是一致的

重排

編譯器在生成二進制代碼的時候,默認按照鏈接的Object File(.o)順序寫文件,按照Object File內部的函數順序寫函數。

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

問題分析:假設我們只有兩個 page:page1/page2,其中綠色的method1 和 method3 啟動時候需要調用,為了執(zhí)行對應的代碼,系統(tǒng)必須進行兩個 Page Fault。

PageFaultFlowOne.png

但如果我們把 method1 和 method3 排布到一起,那么只需要一個Page Fault 即可,這就是二進制文件重排的核心原理

PageFaultFlowTwo.png

為了完成重排,有以下幾個問題要解決:

  • 重排效果怎么樣 - 獲取啟動階段的page fault次數
  • 重排成功了沒 - 拿到當前二進制的函數布局
  • 如何重排 - 讓鏈接器按照指定順序生成Mach-O
  • 重排的內容 - 獲取啟動時候用到的函數

獲取啟動階段的page fault次數

System Trace

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

SystemTraceIDE.png

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

File Backed Page In 即為 Page Fault 的個數

拿到當前二進制的函數布局

可以通過Map Link 獲取到當前的二進制函數布局表

讓鏈接器按照指定順序生成Mach-O

ld

Xcode 使用的鏈接器件是ld,ld有一個不常用的參數 -order_file,通過man ld可以看到詳細文檔:

Alters the order in which functions and data are laid out.For each section in the output file, any symbol in that sec-tion that are specified in the order file file is moved to the start of its section and laid out in the same order as in the order file file. Order files are text files with one symbol name per line. Lines starting with a # are comments. A symbol name may be optionally preceded with its object file leaf name and a colon (e.g. foo.o:_foo). This is useful for static functions/data that occur in multiple files. A symbol name may also be optionally preceded with the architecture (e.g. ppc:_foo or ppc:foo.o:_foo). This enables you to have one order file that works for multiple architectures. Lit-eral c-strings may be ordered by by quoting the string (e.g."Hello, world\n") in the order file.

可以看到,order_file中的符號會按照順序排列在對應section的開始,完美的滿足了我們的需求。

Xcode的GUI也提供了order_file選項


orderFileXcode.png

Xcode 的連接器 ld 默認忽略 order file不存在的方法
如果在 Other Linker Flags: Debug 中添加-order_file_statistics,會以 warning 的形式把這些沒找到的符號打印在日志里

獲取啟動調用的函數符號

Clang官方文檔

  • LLVM支持我們在添加編譯選項 -fsanitize-coverage=trace-pc-guard 的時候,編譯時幫我們在函數中插入__sanitizer_cov_trace_pc_guard,當函數調用的時候,會callq__sanitizer_cov_trace_pc_guard
  • 利用 __builtin_return_address(0) 來獲得當前函數返回地址,也就是調用方的地址。
  • 通過 dladdr 來將指針解析成 Dl_info 結構體信息,其中dli_sname 就是符號的名稱

簡單來說 SanitizerCoverage 是 Clang 內置的一個代碼覆蓋工具。它把一系列以 __sanitizer_cov_trace_pc_ 為前綴的函數調用插入到用戶定義的函數里,借此實現了全局 AOP 的大殺器。其覆蓋之廣,包含 Swift/Objective-C/C/C++ 等語言,Method/Function/Block 全支持。

Build Settings:

在App 的 Target - Build Settings - Other C Flags Debug 添加 -fsanitize-coverage=func,trace-pc-guard

OC - Swift 混編,則在 Other Swift Flags Debug 添加 -sanitize-coverage=func-sanitize=undefined

Cocoapods 管理的第三方庫 可以通過Pod提供的hook來修改所有的pod庫的編譯選項

代碼如下: 在hock方法 post_install 里面做修改配置的操作

post_install do |installer| 
    # 二進制重排設置
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      if config.name == 'Debug'
        config.build_settings['OTHER_CFLAGS'] ||= '$(inherited)'
        config.build_settings['OTHER_CFLAGS'] << ' '
        config.build_settings['OTHER_CFLAGS'] << '-fsanitize-coverage=func,trace-pc-guard'
      end
    end
  end
end
  # 修改主工程的配置
  app_project = Xcodeproj::Project.open(Dir.glob('*.xcodeproj')[0])

  # 主工程二進制重排設置
  app_project.native_targets.each do |target|
    if target.name == '主工程的名稱'
      target.build_configurations.each do |config|
        if config.name == 'Debug'
          config.build_settings['OTHER_CFLAGS'] ||= '$(inherited)'
          config.build_settings['OTHER_CFLAGS'] << ' '
          config.build_settings['OTHER_CFLAGS'] << '-fsanitize-coverage=func,trace-pc-guard'
        end
      end
    end
  end

我們就需要拿到clang的回調來進行函數轉化

代碼直接附上


#import <dlfcn.h>
#import <libkern/OSAtomicQueue.h>
#import <pthread.h>

// 隊列頭的數據結構。
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;

static BOOL collectFinished = NO;

typedef struct {
    void *pc;
    void *next;
} PCNode;

// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint32_t Counter;  // Counter for the guards.
    if (start == stop || *start) return;  // Initialize only once.
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++){
        *x = ++Counter;  // Guards should start from 1.
    }
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
// 該回調由編譯器插入到
// 控制流(適用一些優(yōu)化)
// 通常,編譯器將發(fā)出如下代碼:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (collectFinished || !*guard) {
        return;
    }
    // If you set *guard to 0 this code will not be called again for this edge.
    // Now you can get the PC and do whatever you want:
    //   store it somewhere or symbolize it and print right away.
    // The values of `*guard` are as you set them in
    // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
    // and use them to dereference an array or a bit vector.
    *guard = 0;
    // __builtin_return_address(0)的含義是,得到當前函數返回地址,即此函數被別的函數調用,然后此函數執(zhí)行完畢后,返回,所謂返回地址就是那時候的地址
    void *PC = __builtin_return_address(0);
    PCNode *node = malloc(sizeof(PCNode));
    *node = (PCNode){PC, NULL};
    OSAtomicEnqueue(&queue, node, offsetof(PCNode, next));
}

extern void AppOrderFiles(void(^completion)(NSString *orderFilePath)) {
    collectFinished = YES;
    __sync_synchronize();
    NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSMutableArray <NSString *> *functions = [NSMutableArray array];
        while (YES) {
            PCNode *node = OSAtomicDequeue(&queue, offsetof(PCNode, next));
            if (node == NULL) {
                break;
            }
            Dl_info info = {0};
            dladdr(node->pc, &info);
            if (info.dli_sname) {
                NSString *name = @(info.dli_sname);
                BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
                NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
                [functions addObject:symbolName];
            }
        }
        if (functions.count == 0) {
            if (completion) {
                completion(nil);
            }
            return;
        }
        NSMutableArray<NSString *> *calls = [NSMutableArray arrayWithCapacity:functions.count];
        NSEnumerator *enumerator = [functions reverseObjectEnumerator];
        NSString *obj;
        while (obj = [enumerator nextObject]) {
            if (![calls containsObject:obj]) {
                [calls addObject:obj];
            }
        }
        [calls removeObject:functionExclude];
        NSString *result = [calls componentsJoinedByString:@"\n"];
        NSLog(@"Order:\n%@", result);
        
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"app.order"];
        NSData *fileContents = [result dataUsingEncoding:NSUTF8StringEncoding];
        BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath
                                                               contents:fileContents
                                                             attributes:nil];
    });
}

在想要結束的地方執(zhí)行一下AppOrderFiles()代碼,就可以得到這句代碼執(zhí)行之前的所有的函數執(zhí)行棧的記錄

使用Xcode Run一下后會在沙盒tmp文件夾下面生成app.order文件

設置 order file

在Xcode中設置OrderFile的路徑

PS:配置好 order file 之后,記得清理前面 Build SettingsPodfile 中與 Clang 相關的配置

可以使用System Trace驗證一下前后 File Backed Page In 的次數

學習和實踐過程中的參考文獻:

https://www.cnblogs.com/chengxyyh/archive/2020/06/12/13099407.html

https://blog.csdn.net/chouju2014/article/details/100657380

http://www.itdecent.cn/p/f9b305e2823d

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容