重學OC第二十四篇:啟動優化

SofunNiu發表於2020-11-24

前言

啟動分為冷啟動和熱啟動,主要區別是記憶體是否有APP載入的資料,如果所有的資料需從硬碟讀取後載入到記憶體,那就為冷啟動。下面主要是關於冷啟動方面的優化。

一、冷啟動

1.1 效能檢測

APP啟動分兩個階段來測試:

  • main函式前(pre-main)
    主要是dyld流程部分,包括動態庫載入、類的載入、C++靜態物件處理等。通過在Xcode中新增環境變數DYLD_PRINT_STATISTICS為YES
    在這裡插入圖片描述
//模擬器資料不真實,僅參考
Total pre-main time: 126687488.8 seconds (0.0%)   //總耗時
	dylib loading time: 258.93 milliseconds (0.0%) //dyld載入耗時,官方建議自定義動態庫不超過6個
	rebase/binding time: 126687487.6 seconds (0.0%)//重定址和符號繫結耗時
   	ObjC setup time: 632.00 milliseconds (0.0%)//OC類註冊耗時
   	initializer time: 366.95 milliseconds (0.0%)//+load、c++構造初始化耗時
   	slowest intializers :  //最慢的初始化器
  • main函式後
    從main()函式開始至applicationWillFinishLaunching結束,我們統一稱為main()函式之後的部分。利用錨點分析applicationWillFinishLaunching的耗時。

1.2 優化

官方建議的啟動時間要求:

  • 應該在400ms內完成main()函式之前的載入
  • 整體過程耗時不能超過20秒,否則系統會kill掉程式,App啟動失敗

對於main函式前的優化

  • 減少OC類,移除不需要的類
  • 減少動態庫,移除不需要的動態庫。可通過動態庫合併來讓動態庫數量不超過6個
  • 儘量不要寫__attribute__((constructor))的C函式,也儘量不要用到C++的靜態物件
  • 儘量不要使用+load
  • 壓縮圖片大小,減少I/O操作量

對於main函式後的優化

  • 延遲不必要的頁面、配置等載入
  • 耗時操作考慮多執行緒非同步操作
  • 使用二進位制重排來減少啟動時硬碟到記憶體的操作次數。

二、二進位制重排

二進位制重排指的是重排編譯階段的程式碼檔案和函式的順序。要了解二進位制重排需瞭解虛擬記憶體,相關知識可參考《深入理解計算機系統》第9章虛擬記憶體中的內容。

2.1 原理

編譯器生成二進位制程式碼時,預設按照連結的.o順序寫檔案,按照.o內部順序寫函式,如果函式跨頁了,就會觸發多次Page Fault,所以把函式排到一個Page裡,只需要一次Page Fault。(iOS中每頁大小為16KB)

2.1.1 PageFault檢測

通過Instruments中的Systme Trace,選擇專案,選中main thread,選擇Virtual Memory,檢視File Backed Page In(PageFault)和其他的相關次數、時間等。

2.2 重排

要進行重排要看到二進位制資料的順序,如何進行重排。

2.2.1 二進位制符號順序檢視

Build Settings中搜尋link Map,設定Write Link Map File為YES,然後編譯,在檔案中向上兩級找到Intermediates.noindex,進入xxx.build最後找到xxx-LinkMap-normal-x86_64.txt檔案,開啟找到Symbol可看到按編譯時檔案和檔案內部函式書寫順序排列。

2.2.2 通過.order檔案重排

建立xxx.order檔案,在Build Settings中搜尋order file,加入./xxx.order,然後編譯。
那xxx.order檔案中的內容怎麼來,可通過clang插樁hook函式然後儲存到xxx.order中。

2.2.3 Clang插樁

官方介紹: https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs

SanitizerCoverage 是 Clang 內建的一個程式碼覆蓋工具,它把一系列以 _sanitizer_cov_trace_pc 為字首的函式呼叫插入到使用者定義的函式裡來實現全域性 AOP。其覆蓋之廣,包含 Swift/Objective-C/C/C++ 等語言,Method/Function/Block 全支援。

使用步驟:

  1. Build Settings 搜尋 Other C Flags,新增-fsanitize-coverage=func, trace-pc-guard引數(OC); Other Swift Flags新增-sanitize-coverage=func 和 -sanitize=undefined(swift)
  2. 在__sanitizer_cov_trace_pc_guard_init和__sanitizer_cov_trace_pc_guard_init方法中進行相關程式碼編寫或直接使用第三方AppOrderFiles
  3. 在didFinishLaunchingWithOptions中呼叫相應的方法生成xxx.order檔案並進行呼叫順序重排
  4. 把重排後的xxx.order檔案拷貝到專案的根目錄下,在Build Settings中搜尋order file並加入./xxx.order或者${SRCROOT}/xxx.order

AppOrderFiles原始碼

#import "AppOrderFiles.h"
#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 N;  // 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 = ++N;  // 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);
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;
    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);  //把資訊讀入info中
            if (info.dli_sname) {
                NSString *name = @(info.dli_sname);  
                BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];   //OC方法
                NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name]; //非OC方法加_字首
                [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];   //寫入檔案
        if (completion) {
            completion(success ? filePath : nil);
        }
    });
}

總結

  • 對冷啟動效能的檢測,在Xcode中新增環境變數DYLD_PRINT_STATISTICS為YES來列印pre-main階段的耗時
  • 建議優化應該在400ms內完成main()函式之前的載入,整體過程耗時不能超過20秒
  • 通過Instruments中的Systme Trace檢視PageFault
  • Build Settings中搜尋link Map,設定Write Link Map File為YES,然後編譯,在檔案中向上兩級找到Intermediates.noindex,進入xxx.build最後找到xxx-LinkMap-normal-x86_64.txt檔案檢視二進位制符號
  • 通過Clang插樁最後生成.order檔案,把重排後的xxx.order檔案拷貝到專案的根目錄下,在Build Settings中搜尋order file並加入${SRCROOT}/xxx.order。

參考文章:
App 二進位制檔案重排已經被玩壞了
Improving Locality of Reference

相關文章