07 - iOS啟動優(yōu)化 - 二進制重排 & Clang插樁

啟動的過程一般是指從用戶點擊app圖標開始到AppDelegate 的didFinishLaunching方法執(zhí)行完成為止,其中,啟動也分為冷啟動熱啟動

冷啟動:內存中不包含app相關數據的啟動,一般我們可以通過重啟手機來實現冷啟動

  • 1 pre-main階段,即main函數之前,操作系統(tǒng)加載App可執(zhí)行文件到內存,執(zhí)行一系列的加載&鏈接等工作,簡單來說,就是dyld加載過程
  • 2:main函數之后,即從main函數開始,到Appdelegate 的didFinishLaunching方法執(zhí)行完成為止,主要是構建第一個界面,并完成渲染
    -熱啟動:是指殺掉app進程后,數據仍然存在時的啟動

其中1,2 兩過程 就是 從用戶點擊App圖標到用戶能看到app主界面的過程,即需要啟動優(yōu)化的部分

pre-main階段的優(yōu)化

統(tǒng)計main函數啟動時間

  • 查看系統(tǒng)給的反饋需要增加一個環(huán)境變量,
  • 增加路徑:在 Xcode -> Edit Scheme -> Run -> Arguments -> Environment Variables 中,
  • 增加一個環(huán)境變量DYLD_PRINT_STATISTICS:1。
pre-main啟動耗時

-dylib loading time(動態(tài)庫耗時):主要是加載動態(tài)庫,系統(tǒng)的動態(tài)庫做過優(yōu)化,耗時較少。蘋果官方推薦最多不要超過6個外部動態(tài)庫,多余6個,需要考慮合并動態(tài)庫,合并動態(tài)庫對于啟動時期的優(yōu)化,非常有效
-rebase/binding time(偏移修正/符號綁定耗時)
-rebase(偏移修正):任何一個app生成的二進制文件,在二進制文件內部所有的方法、函數調用,都有一個地址,這個地址是在當前二進制文件中的偏移地址。一旦在運行時刻(即運行到內存中),每次系統(tǒng)都會隨機分配一個ASLRAddress Space Layout Randomization,地址空間布局隨機化)地址值(是一個安全機制,會分配一個隨機的數值,插入在二進制文件的開頭),例如,二進制文件中有一個 test方法,偏移值是0x0001,而隨機分配的ASLR是0x1f00,如果想訪問test方法,其內存地址(即真實地址)變?yōu)?ASLR+偏移值 = 運行時確定的內存地址(即0x1f00+0x0001 = 0x1f01
-binding(綁定):,例如NSLog方法,在編譯時期生成的mach-o文件中,會創(chuàng)建一個符號!NSLog(目前指向一個隨機的地址),然后在運行時(從磁盤加載到內存中,是一個鏡像文件),會將真正的地址給符號(即在內存中將地址與符號進行綁定,是dyld做的,也稱為動態(tài)庫符號綁定),一句話概括:綁定就是給符號賦值的過程

  • Objc setup time: 注冊所有OC類耗時,類越多耗時越多,有人統(tǒng)計過2萬個自定義的OC的類,大概耗時800毫秒。刪除不用的類,可以減少耗時
  • initializer time: load方法 和C++構造函數的耗時. 減少重寫load方法,盡量將事情延遲到 main 方法以后,可以減少耗時。

優(yōu)化建議

  • 減少外部動態(tài)庫的數量
    -減少OC類,因為OC類越多,越耗時
    -將不必須在+load方法中做的事情延遲到+initialize中,盡量不要用C++虛函數
  • 啟動階段能使用多線程來初始化的,就使用多線程
  • 使用純代碼。不用xib storyboard(要額外進行代碼解析轉換和頁面的渲染)

二進制重排

原理

當進程訪問一個虛擬內存page,而對應的物理內存不存在時,會觸發(fā)缺頁中斷Page Fault),因此阻塞進程。此時就需要先加載數據到物理內存,然后再繼續(xù)訪問

App在冷啟動過程中,會有大量的類、分類、三方等需要加載和執(zhí)行,此刻需要調用的方法,處于不同的Page導致的此時的產生的Page Fault所帶來的的耗時是很大的

將所有啟動時刻需要調用的方法,排列在一起,即放在一個頁中,這樣就從多個Page Fault變成了一個Page Fault,優(yōu)化了啟動時間

監(jiān)測

查看一下項目的缺頁異常數量。注意需要卸載 APP 或者重啟手機,來保證這個APP完全沒有被加載到內存中
打開Instrument -> System Trace

工具

從圖中可以看出發(fā)生的PageFault427

優(yōu)化后

優(yōu)化后項目的缺頁遺產數量是286, 減少了啟動時大概40%的缺頁異常~

通過linkMap也可以查看啟動方法的順序

  • Build Setting -> Write Link Map File設置為YES

    設置

  • 編譯,然后在對應的路徑下查找link map文件,如下所示,可以發(fā)現 類中函數的加載順序是從上到下的,而文件的順序是根據Build Phases -> Compile Sources中的順序加載的

    加載順序

二進制重排的方法
1.方法的重排序

xcode已經為我們提供了這個機制,它使用的鏈接器叫做ld, ld有一個參數叫做Order File, 我們可以通過配置order文件,來使編譯時生成的二進制的文件的Link Map種的符號順序,按照我們指定的順序排列生成。而且libobjc 實際上也做了二進制重排。

order文件

【第一步】在項目根目錄下建一個xxx.order的文件,里面寫上按照自己想排列的順序,寫上方法或者函數的名字。(如果寫了一個不存在的符號,也不會報錯,會被自動過濾掉~)

  • 內容).png

【第三步】重新編譯,查看 Link Map文件的順序,果然,按照我們指定的順序排列啦!

排列后啟動順序

找到啟動時需要方法 -> 靜態(tài)插樁

接下來,需要做的就是寫入 order 文件里的符號了,我們不可能手寫上所有的啟動時需要的執(zhí)行的符號,這里的所有符號包括,調用的方法、函數、C++構造方法、swift方法、block。

這里使用 LLVM 內置的簡單代碼覆蓋率檢測工具 [圖片上傳失敗...(image-b6c749-1626686913498)]。它在邊緣、函數、基本塊``級別`上插入對用戶定義函數的調用。

  • edge (默認):檢測邊緣(所有的指令跳轉都會被插入對用戶定義函數的調用, 如循環(huán)、分支判斷、方法函數等)。
    bb檢測基本塊。

  • func:僅將檢測每個功能的輸入塊(這個就是我們要重排序的符號)。

按照文檔,

  • 【第1步】搜索并設置 Other C Flags/ Other C++ Flags-fsanitize-coverage=func,trace-pc-guard這里要用func, 不能用默認的edge, 不然會造成死循環(huán))。

  • 如果有swift ,需要設置 Other Swift Flags設置為 -sanitize-coverage=func -sanitize=undefined

    設置

  • 【第2步】編譯器將插入對模塊構造函數的調用,所以我們要實現這個方法:

__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
  -  參數1 start 是一個指針,指向無符號int類型,4個字節(jié),相當于一個數組的起始位置,即符號的起始位置(是從高位往低位讀)

  - 參數2 stop,由于數據的地址是往下讀的(即從高往低讀,所以此時獲取的地址并不是stop真正的地址,而是標記的最后的地址,讀取stop時,由于stop占4個字節(jié),stop真實地址 = stop打印的地址-0x4)
  • 【第3步】__sanitizer_cov_trace_pc_guard 實現: 主要是捕獲所有的啟動時刻符號,將所有符號入隊
//原子隊列,其目的是保證寫入安全,線程安全
static  OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定義符號結構體,以鏈表的形式
typedef struct {
    void *pc;
    void *next;
}XXNode;

/*
 - start:起始位置
 - stop:并不是最后一個符號的地址,而是整個符號表的最后一個地址,最后一個符號的地址=stop-4(因為是從高地址往低地址讀取的,且stop是一個無符號int類型,占4個字節(jié))。stop存儲的值是符號的
 */
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    printf("INIT: %p - %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++) {
        *x = ++N;
    }
    
}

/*
 可以全面hook方法、函數、以及block調用,用于捕捉符號,是在多線程進行的,這個方法中只存儲pc,以鏈表的形式
 
 - guard 是一個哨兵,告訴我們是第幾個被調用的
 */
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return;//將load方法過濾掉了,所以需要注釋掉
    
    //獲取PC
    /*
     - PC 當前函數返回上一個調用的地址
     - 0 當前這個函數地址,即當前函數的返回地址
     - 1 當前函數調用者的地址,即上一個函數的返回地址
    */
    void *PC = __builtin_return_address(0);
    //創(chuàng)建node,并賦值
    XXNode *node = malloc(sizeof(XXNode));
    *node = (XXNode){PC, NULL};
    
    //加入隊列
    //符號的訪問不是通過下標訪問,是通過鏈表的next指針,所以需要借用offsetof(結構體類型,下一個的地址即next)
    OSAtomicEnqueue(&queue, node, offsetof(XXNode, next));
}

-【第四步:獲取所有符號并寫入文件】

extern void getOrderFile(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), ^{
        //創(chuàng)建符號數組
        NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
        
        //while循環(huán)取符號
        while (YES) {
            //出隊
            CJLNode *node = OSAtomicDequeue(&queue, offsetof(CJLNode, next));
            if (node == NULL) break;
            
            //取出PC,存入info
            Dl_info info;
            dladdr(node->pc, &info);
//            printf("%s \n", info.dli_sname);
            
            if (info.dli_sname) {
                //判斷是不是OC方法,如果不是,需要加下劃線存儲,反之,則直接存儲
                NSString *name = @(info.dli_sname);
                BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
                NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
                [symbolNames addObject:symbolName];
            }
           
        }
        
        if (symbolNames.count == 0) {
            if (completion) {
                completion(nil);
            }
            return;
        }
        
        //取反(隊列的存儲是反序的)
        NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
        
        //去重
        NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
        NSString *name;
        while (name = [emt nextObject]) {
            if (![funcs containsObject:name]) {
                [funcs addObject:name];
            }
        }
        
        //去掉自己
        [funcs removeObject:functionExclude];
        
        //將數組變成字符串
        NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
        NSLog(@"Order:\n%@", funcStr);
        
        //字符串寫入文件
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cjl.order"];
        NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
        BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
        if (completion) {
            completion(success ? filePath : nil);
        }
    });
}
  • 第五步:在didFinishLaunchingWithOptions方法最后調用】需要注意的是,這里的調用位置是由你決定的,一般來說,是第一個渲染的界面
  • 【第六步:拷貝文件,放入指定位置,并配置路徑】一般將該文件放入主項目路徑下,并在Build Settings -> Order File中配置./XX.order,下面是配置前后的對比(上邊是配置前的熟悉怒,下邊是配置后符號順序的)
結果對比

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容