啟動(dòng)優(yōu)化--二進(jìn)制重排

啟動(dòng)優(yōu)化--二進(jìn)制重排

啟動(dòng)優(yōu)化

  1. 啟動(dòng)時(shí)間:從用戶點(diǎn)擊app圖標(biāo)開(kāi)始到 AppDelegate 的didFinishLaunching
  2. 冷啟動(dòng): 內(nèi)存中不包含app相關(guān)數(shù)據(jù)的啟動(dòng),一般我們可以通過(guò)重啟手機(jī)來(lái)實(shí)現(xiàn)冷啟動(dòng)
    1. 熱啟動(dòng): 是指殺掉app進(jìn)程后,數(shù)據(jù)仍然存在時(shí)的啟動(dòng)
  3. 啟動(dòng)優(yōu)化 -> T1 + T2 需要啟動(dòng)優(yōu)化的部分
    1. T1: pre-main階段, 即main函數(shù)之前, 操作系統(tǒng)加載APP可執(zhí)行文件到內(nèi)存,執(zhí)行一系列加載&鏈接等工作 -> dyld加載過(guò)程.
    2. main函數(shù)之后, 即從main函數(shù)開(kāi)始, 到Appdelegate到didFinishLaunching方法執(zhí)行完成為止, 主要是構(gòu)建第一個(gè)界面,并完成渲染.

main函數(shù)之前的部分

Edit Scheme -> Run -> Arguments -> Environment Variables -> 添加'DYLD_PRINT_STATISTICS'設(shè)為1

image.png

pre-main字段說(shuō)明

  • dylib loading time(動(dòng)態(tài)庫(kù)耗時(shí))
  • rebase/binding time(偏移修正/符號(hào)綁定耗時(shí))
    • rebase(偏移修正):任何一個(gè)app生成的二進(jìn)制文件,在二進(jìn)制文件內(nèi)部所有的方法、函數(shù)調(diào)用,都有一個(gè)地址,這個(gè)地址是在當(dāng)前二進(jìn)制文件中的偏移地址。一旦在運(yùn)行時(shí)刻(即運(yùn)行到內(nèi)存中),每次系統(tǒng)都會(huì)隨機(jī)分配一個(gè)ASLR(Address Space Layout Randomization,地址空間布局隨機(jī)化)地址值(是一個(gè)安全機(jī)制,會(huì)分配一個(gè)隨機(jī)的數(shù)值,插入在二進(jìn)制文件的開(kāi)頭),例如,二進(jìn)制文件中有一個(gè) test方法,偏移值是0x0001,而隨機(jī)分配的ASLR是0x1f00,如果想訪問(wèn)test方法,其內(nèi)存地址(即真實(shí)地址)變?yōu)?ASLR+偏移值 = 運(yùn)行時(shí)確定的內(nèi)存地址(即0x1f00+0x0001 = 0x1f01)
    • binding(綁定):,例如NSLog方法,在編譯時(shí)期生成的mach-o文件中,會(huì)創(chuàng)建一個(gè)符號(hào)!NSLog(目前指向一個(gè)隨機(jī)的地址),然后在運(yùn)行時(shí)(從磁盤加載到內(nèi)存中,是一個(gè)鏡像文件),會(huì)將真正的地址給符號(hào)(即在內(nèi)存中將地址與符號(hào)進(jìn)行綁定,是dyld做的,也稱為動(dòng)態(tài)庫(kù)符號(hào)綁定),一句話概括:綁定就是給符號(hào)賦值的過(guò)程
  • ObjC setup time (OC類注冊(cè)的耗時(shí)):OC類越多,越耗時(shí)
  • initializer time(執(zhí)行l(wèi)oad和構(gòu)造函數(shù)的耗時(shí))

優(yōu)化建議:

  1. 盡量少用外部動(dòng)態(tài)庫(kù),蘋果官方建議自定義的動(dòng)態(tài)庫(kù)最好不要超過(guò)6個(gè),如果超過(guò)6個(gè),需要合并動(dòng)態(tài)庫(kù)
  2. OC類越多越耗時(shí)
  3. 將不必須在+load方法中做的事情延遲到+initialize中,盡量不要用C++虛函數(shù)
  4. 如果是swift,盡量使用struct

main函數(shù)階段的優(yōu)化

didFinishLaunching方法中,主要是執(zhí)行了各種業(yè)務(wù),有很多并不是必須在這里立即執(zhí)行的,這種業(yè)務(wù)我們可以采取延遲加載,防止影響啟動(dòng)時(shí)間。

didFinishLaunching中業(yè)務(wù)主要類型
  1. 【第一類】初始化第三方sdk
  2. 【第二類】app運(yùn)行環(huán)境配置
  3. 【第三類】自己工具類的初始化等

main函數(shù)階段的優(yōu)化建議:

  • 減少啟動(dòng)初始化的流程,能懶加載的懶加載,能延遲的延遲,能放后臺(tái)初始化的放后臺(tái),盡量不要占用主線程的啟動(dòng)時(shí)間
  • 優(yōu)化代碼邏輯,去除非必須的代碼邏輯,減少每個(gè)流程的消耗時(shí)間
  • 啟動(dòng)階段能使用多線程來(lái)初始化的,就使用多線程
  • 盡量使用純代碼來(lái)進(jìn)行UI框架的搭建,尤其是主UI框架,例如UITabBarController。盡量避免使用Xib或者SB,相比純代碼而言,這種更耗時(shí)
  • 刪除廢棄類、方法

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

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

查看當(dāng)前項(xiàng)目的缺頁(yè)終端

  1. cmd + i 性能分析, 需要讓子彈飛一會(huì)兒
  2. 選擇System Trace
  3. 如下圖配置查看缺頁(yè)中斷次數(shù)
image.png

從上面的Page Fault的次數(shù)以及加載順序,可以發(fā)現(xiàn)其實(shí)導(dǎo)致Page Fault次數(shù)過(guò)多的根本原因是啟動(dòng)時(shí)刻需要調(diào)用的方法,處于不同的Page導(dǎo)致的。因此,我們的優(yōu)化思路就是:將所有啟動(dòng)時(shí)刻需要調(diào)用的方法,排列在一起,即放在一個(gè)頁(yè)中,這樣就從多個(gè)Page Fault變成了一個(gè)Page Fault。這就是二進(jìn)制重排的核心原理

查看文件執(zhí)行順序

  1. Build Setting -> Write Link Map File設(shè)置為YES, 如下圖配置


    image.png
  2. CMD+B編譯demo,然后在對(duì)應(yīng)的路徑下查找 link map文件,如下所示,可以發(fā)現(xiàn) 類中函數(shù)的加載順序是從上到下的,而文件的順序是根據(jù)Build Phases -> Compile Sources中的順序加載的

  3. Link Map是iOS編譯過(guò)程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要在Xcode的Build Settings里開(kāi)啟Write Link Map File

    1. Object Files 生成二進(jìn)制用到的link單元的路徑和文件編號(hào)
    2. Sections 記錄Mach-O每個(gè)Segment/section的地址范圍
    3. Symbols 按順序記錄每個(gè)符號(hào)的地址范圍

ld

ld是Xcode使用的鏈接器,有一個(gè)參數(shù)order_file,我們可以通過(guò)在Build Settings -> Order File配置一個(gè)后綴為order的文件路徑。在這個(gè)order文件中,將所需要的符號(hào)按照順序?qū)懺诶锩?,在?xiàng)目編譯時(shí),會(huì)按照這個(gè)文件的順序進(jìn)行加載,以此來(lái)達(dá)到我們的優(yōu)化 -> 二進(jìn)制重排的本質(zhì)就是對(duì)啟動(dòng)加載的符號(hào)進(jìn)行重新排列.

獲取啟動(dòng)運(yùn)行的函數(shù)呢
  1. hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息,在底層都會(huì)來(lái)到objc_msgSend,但是由于objc_msgSend的參數(shù)是可變的,需要通過(guò)匯編獲取,對(duì)開(kāi)發(fā)人員要求較高。而且也只能拿到OC 和 swift中@objc 后的方法
  2. 靜態(tài)掃描:掃描 Mach-O 特定段和節(jié)里面所存儲(chǔ)的符號(hào)以及函數(shù)數(shù)據(jù)
  3. Clang插樁:即批量hook,可以實(shí)現(xiàn)100%符號(hào)覆蓋,即完全獲取swift、OC、C、block函

Clang 插樁

llvm內(nèi)置了一個(gè)簡(jiǎn)單的代碼覆蓋率檢測(cè)(SanitizerCoverage)。它在函數(shù)級(jí)、基本塊級(jí)和邊緣級(jí)插入對(duì)用戶定義函數(shù)的調(diào)用。我們這里的批量hook,就需要借助于SanitizerCoverage。
SanitizerCoverage官方文檔

AppOrderFiles

  1. 使用AppOrderFiles
  2. 在 Build Settings 里的 “Other C Flags” 中添加 -fsanitize-coverage=func,trace-pc-guard
    1. 如果是Swift項(xiàng)目,還需要額外在 “Other Swift Flags” 中加入-sanitize-coverage=func 和 -sanitize=undefined
//當(dāng)然通過(guò)pod導(dǎo)入的, 可以在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'
      config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-coverage=func -sanitize=undefined'
    end
  end
end

源碼解析

//原子隊(duì)列,其目的是保證寫入安全,線程安全
static  OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體,以鏈表的形式
typedef struct {
    void *pc;
    void *next;
}CJLNode;

/*
 - start:起始位置
 - stop:并不是最后一個(gè)符號(hào)的地址,而是整個(gè)符號(hào)表的最后一個(gè)地址,最后一個(gè)符號(hào)的地址=stop-4(因?yàn)槭菑母叩刂吠偷刂纷x取的,且stop是一個(gè)無(wú)符號(hào)int類型,占4個(gè)字節(jié))。stop存儲(chǔ)的值是符號(hào)的
 */
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方法、函數(shù)、以及block調(diào)用,用于捕捉符號(hào),是在多線程進(jìn)行的,這個(gè)方法中只存儲(chǔ)pc,以鏈表的形式
 
 - guard 是一個(gè)哨兵,告訴我們是第幾個(gè)被調(diào)用的
 */
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return;//將load方法過(guò)濾掉了,所以需要注釋掉
    
    //獲取PC
    /*
     - PC 當(dāng)前函數(shù)返回上一個(gè)調(diào)用的地址
     - 0 當(dāng)前這個(gè)函數(shù)地址,即當(dāng)前函數(shù)的返回地址
     - 1 當(dāng)前函數(shù)調(diào)用者的地址,即上一個(gè)函數(shù)的返回地址
    */
    void *PC = __builtin_return_address(0);
    //創(chuàng)建node,并賦值
    CJLNode *node = malloc(sizeof(CJLNode));
    *node = (CJLNode){PC, NULL};
    
    //加入隊(duì)列
    //符號(hào)的訪問(wèn)不是通過(guò)下標(biāo)訪問(wèn),是通過(guò)鏈表的next指針,所以需要借用offsetof(結(jié)構(gòu)體類型,下一個(gè)的地址即next)
    OSAtomicEnqueue(&queue, node, offsetof(CJLNode, next));
}

獲取所有符號(hào)并寫入文件
注意:只要是匯編中的跳轉(zhuǎn)都會(huì)被hook, 既有b,bl的指令都會(huì)被hook

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)建符號(hào)數(shù)組
        NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
        
        //while循環(huán)取符號(hào)
        while (YES) {
            //出隊(duì)
            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方法,如果不是,需要加下劃線存儲(chǔ),反之,則直接存儲(chǔ)
                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;
        }
        
        //取反(隊(duì)列的存儲(chǔ)是反序的)
        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];
        
        //將數(shù)組變成字符串
        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);
        }
    });
}

配置order文件

通過(guò)上面的方法生成order文件后, 配置 -> Build Setting -> Order File -> ./xxx.order

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容