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

熱啟動(dòng)與冷啟動(dòng)

  • 冷啟動(dòng)App點(diǎn)擊啟動(dòng)前,此時(shí)App的進(jìn)程還不在系統(tǒng)里,內(nèi)存中不包含app相關(guān)數(shù)據(jù),需要系統(tǒng)新創(chuàng)建一個(gè)進(jìn)程分配給App。
  • 熱啟動(dòng)App冷啟動(dòng)后用戶將App退回后臺(tái),此時(shí)App的進(jìn)程還在系統(tǒng)里,數(shù)據(jù)仍然存在,用戶重新返回App的過程。

APP冷啟動(dòng)完整流程

冷啟動(dòng)的整個(gè)過程是指從用戶喚起 App開始到 AppDelegate 中的 didFinishLaunchingWithOptions 方法執(zhí)行完畢為止,并以執(zhí)行main()函數(shù)的時(shí)機(jī)為分界點(diǎn),分為pre-mainmain()兩個(gè)階段。

pre-main階段

pre-main階段,即main函數(shù)之前,操作系統(tǒng)加載App可執(zhí)行文件到內(nèi)存,執(zhí)行一系列的加載&鏈接等工作,簡(jiǎn)單來說,就是dyld加載過程

2251862-eb6d2c7572f89693.jpg

  • dylib loading time(動(dòng)態(tài)庫(kù)耗時(shí)):主要是加載動(dòng)態(tài)庫(kù)
  • rebase/binding time(偏移修正/符號(hào)綁定耗時(shí)):進(jìn)行rebase指針調(diào)整和bind符號(hào)綁定
    • rebase:任何一個(gè)app生成的二進(jìn)制文件,所有的方法和函數(shù)調(diào)用都對(duì)應(yīng)一個(gè)地址,這個(gè)地址就是當(dāng)前二進(jìn)制文件中的偏移地址,為了安全,通常會(huì)隨機(jī)分配一個(gè)ASLR隨機(jī)值,只有ASLR+偏移地址才是方法或函數(shù)對(duì)內(nèi)存的真實(shí)地址
    • binding:將編譯期創(chuàng)建的符號(hào)運(yùn)行時(shí)地址進(jìn)行綁定,一般是dyld執(zhí)行,也可以叫動(dòng)態(tài)庫(kù)符號(hào)綁定
      *ObjC setup time(OC類注冊(cè)的耗時(shí)):ObjC相關(guān)Class、category注冊(cè)、selector唯一性檢查等,OC類越多,時(shí)間越長(zhǎng)
      *initializer time(執(zhí)行l(wèi)oad和構(gòu)造函數(shù)的耗時(shí)):+load()方法、attribute修飾的函數(shù)調(diào)用、創(chuàng)建C++靜態(tài)全局變量等
pre-main優(yōu)化方案
  • 減少外部動(dòng)態(tài)庫(kù)加載,官方建議自定義的動(dòng)態(tài)庫(kù)不要超過6個(gè),如果超過則需要合并動(dòng)態(tài)庫(kù)
  • 減少OC類
  • 將不必須在+load方法中做的事情延遲到+initialize中,盡量不要用C++虛函數(shù)

main函數(shù)

main函數(shù)執(zhí)行后的階段,指的是:從 main函數(shù)執(zhí)行開始,到appDelegatedidFinishLaunchingWithOptions方法里首屏渲染相關(guān)方法執(zhí)行完成。
即,從main函數(shù)執(zhí)行到設(shè)置self.window.rootViewController執(zhí)行完成的階段。

didFinishLaunching中的業(yè)務(wù)主要分為三個(gè)類型

  • 【第一類】初始化第三方sdk
  • 【第二類】app運(yùn)行環(huán)境配置
  • 【第三類】自己工具類的初始化等
main函數(shù)階段優(yōu)化方案
  • 盡量使用純代碼來進(jìn)行UI框架的搭建,尤其是主UI框架,例如UITabBarController。盡量避免使用Xib或者SB,相比純代碼而言,這種更耗時(shí)
  • 將耗時(shí)操作滯后或異步處理。
    通常的耗時(shí)操作有:網(wǎng)絡(luò)加載、編輯、存儲(chǔ)圖片和文件等資源,不要占用主線程時(shí)間
  • main函數(shù)執(zhí)行后到首屏渲染完成前,只處理首屏渲染相關(guān)業(yè)務(wù)。
    首屏渲染外的其他功能放到首屏渲染完成后去初始化
  • 減少啟動(dòng)初始化的流程,能懶加載的懶加載,能延遲的延遲,能放后臺(tái)初始化的放后臺(tái),盡量不要占用主線程的啟動(dòng)時(shí)間
  • 啟動(dòng)階段能使用多線程來初始化的,就使用多線程
  • 刪除廢棄類、方法

二進(jìn)制重排

對(duì)用戶而言,使用App時(shí)第一個(gè)直接體驗(yàn)就是啟動(dòng) App 時(shí)間,而啟動(dòng)時(shí)期會(huì)有大量的類、分類、三方等等需要加載和執(zhí)行,此時(shí)多個(gè) Page Fault 所產(chǎn)生的的耗時(shí)往往是不能小覷的,下面我們就通過二進(jìn)制重排來優(yōu)化啟動(dòng)耗時(shí)。

虛擬內(nèi)存

  • 進(jìn)程物理內(nèi)存之間增加一個(gè)中間層,這個(gè)中間層就是所謂的虛擬內(nèi)存,主要用于解決當(dāng)多個(gè)進(jìn)程同時(shí)存在時(shí),對(duì)物理內(nèi)存的管理。提高了CPU的利用率,使多個(gè)進(jìn)程可以同時(shí)、按需加載。所以虛擬內(nèi)存其本質(zhì)就是一張?zhí)摂M地址和物理地址對(duì)應(yīng)關(guān)系的映射表

  • 每個(gè)進(jìn)程都有一個(gè)獨(dú)立的虛擬內(nèi)存,其地址都是從0開始,大小是4G固定的,每個(gè)虛擬內(nèi)存又會(huì)劃分為一個(gè)一個(gè)的頁(yè)(頁(yè)的大小在iOS中是16K,其他的是4K),每次加載都是以頁(yè)為單位加載的,進(jìn)程間是無法互相訪問的,保證了進(jìn)程間數(shù)據(jù)的安全性。

  • 一個(gè)進(jìn)程中,只有部分功能是活躍的,所以只需要將進(jìn)程中活躍的部分放入物理內(nèi)存,避免物理內(nèi)存的浪費(fèi)

  • 當(dāng)CPU需要訪問數(shù)據(jù)時(shí),首先是訪問虛擬內(nèi)存,然后通過虛擬內(nèi)存去尋址,即可以理解為在表中找對(duì)應(yīng)的物理地址,然后對(duì)相應(yīng)的物理地址進(jìn)行訪問

  • 如果在訪問時(shí),虛擬地址的內(nèi)容未加載到物理內(nèi)存,會(huì)發(fā)生缺頁(yè)異常(pagefault),將當(dāng)前進(jìn)程阻塞掉,此時(shí)需要先將數(shù)據(jù)載入到物理內(nèi)存,然后再尋址,進(jìn)行讀取。這樣就避免了內(nèi)存浪費(fèi)

page Fault 調(diào)試

打開Instruments,選擇 System Trace
選擇真機(jī),選擇工程,選擇啟動(dòng)(啟動(dòng)前最好重啟手機(jī),清除緩存數(shù)據(jù),確保是冷啟動(dòng)狀態(tài)),當(dāng)頁(yè)面加載出來的時(shí)候,停止

二進(jìn)制重排實(shí)踐

二進(jìn)制重排就是對(duì)即將生成的可執(zhí)行文件重新排列,即它發(fā)生在鏈接階段。
首先理解幾個(gè)名詞

  • Link Map:LinkmapiOS編譯過程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要在Xcode的Build Settings里開啟Write Link Map File,Link Map主要包含三部分:
    *Object Files 生成二進(jìn)制用到的link單元路徑和文件編號(hào)
    • Sections記錄Mach-O每個(gè)Segment/section地址范圍
    • Symbols 按順序記錄每個(gè)符號(hào)的地址范圍
  • ld :ld 有一個(gè)參數(shù)叫 Order File,通過Build Settings -> Order File配置一個(gè) 后綴名 為order的文件路徑。在這個(gè)order文件中,將你需要的符號(hào)按順序?qū)懺诶锩?。?dāng)工程build 的時(shí)候,Xcode 會(huì)讀取這個(gè)文件,打的二進(jìn)制包就會(huì)按照這個(gè)文件中的符號(hào)順序進(jìn)行生成對(duì)應(yīng)的 mach-O。

如何獲取啟動(dòng)運(yùn)行的函數(shù)呢

  • hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息,在底層都會(huì)來到objc_msgSend,但是由于objc_msgSend參數(shù)是可變的,需要通過匯編獲取,對(duì)開發(fā)人員要求較高。而且也只能拿到OC 和 swift中@objc 后的方法

  • 靜態(tài)掃描:掃描 Mach-O特定段和節(jié)里面所存儲(chǔ)的符號(hào)以及函數(shù)數(shù)據(jù)

  • Clang插樁:即批量hook,可以實(shí)現(xiàn)100%符號(hào)覆蓋,即完全獲取swift、OC、C、block函數(shù)

Clang插樁

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

  • 開啟SanitizerCoverage
    • OC項(xiàng)目,需要在:在 Build Settings里的 “Other C Flags”中添加 -fsanitize-coverage=func,trace-pc-guard
    • 如果是Swift項(xiàng)目,還需要額外在 “Other Swift Flags”中加入-sanitize-coverage=func-sanitize=undefined
  • 重寫方法
    • __sanitizer_cov_trace_pc_guard_init方法
      • 參數(shù)1start 是一個(gè)指針,指向無符號(hào)int類型,4個(gè)字節(jié),相當(dāng)于一個(gè)數(shù)組的起始位置,即符號(hào)的起始位置(是從高位往低位讀)
      • 參數(shù)2 stop,由于數(shù)據(jù)的地址是往下讀的(即從高往低讀,所以此時(shí)獲取的地址并不是stop真正的地址,而是標(biāo)記的最后的地址,讀取stop時(shí),由于stop占4個(gè)字節(jié),stop真實(shí)地址 = stop打印的地址-0x4)
        *__sanitizer_cov_trace_pc_guard方法
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
//定義原子隊(duì)列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體
typedef struct{
    void *pc;
    void *next;
} SYNode;
/*
 - start:起始位置
 - stop:并不是最后一個(gè)符號(hào)的地址,而是整個(gè)符號(hào)表的最后一個(gè)地址,最后一個(gè)符號(hào)的地址=stop-4(因?yàn)槭菑母叩刂吠偷刂纷x取的,且stop是一個(gè)無符號(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;  // 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.
}
/*
 可以全面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;  // Duplicate the guard check.
//獲取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);
  char PcDescr[1024];
  //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);

 //創(chuàng)建結(jié)構(gòu)體!
   SYNode * node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    
    //加入結(jié)構(gòu)!
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
  • 獲取所有符號(hào)并寫入文件
{
    //定義數(shù)組
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    
    while (YES) {//一次循環(huán)!也會(huì)被HOOK一次!!
       SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        
        if (node == NULL) {
            break;
        }
        Dl_info info = {0};
        dladdr(node->pc, &info);
//        printf("%s \n",info.dli_sname);
        NSString * name = @(info.dli_sname);
        free(node);
        
        BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        //是否去重??
        [symbolNames addObject:symbolName];
        /*
        if ([name hasPrefix:@"+["]||[name hasPrefix:@"-["]) {
            //如果是OC方法名稱直接存!
            [symbolNames addObject:name];
            continue;
        }
        //如果不是OC直接加個(gè)_存!
        [symbolNames addObject:[@"_" stringByAppendingString:name]];
         */
    }
    //反向數(shù)組
//    symbolNames = (NSMutableArray<NSString *>*)[[symbolNames reverseObjectEnumerator] allObjects];
    NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
    
    //創(chuàng)建一個(gè)新數(shù)組
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString * name;
    //去重!
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {//數(shù)組中不包含name
            [funcs addObject:name];
        }
    }
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //數(shù)組轉(zhuǎn)成字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    //字符串寫入文件
    //文件路徑
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
    //文件內(nèi)容
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}

  • 拷貝文件,放入指定位置,并配置路徑
    一般將該文件放入主項(xiàng)目路徑下,并在Build Settings -> Order File中配置./hank.order

  • 注:Build Settings -> Other C Flags的如果配置的是-fsanitize-coverage=trace-pc-guard,沒有func,在while循環(huán)部分會(huì)出現(xiàn)死循環(huán),原因是while循環(huán)也會(huì)被__sanitizer_cov_trace_pc_guard中的guard哨兵檢測(cè)到,通過匯編可以查看到流程

  • 第一次是bltouchBegin

    2251862-3ab80a2744e85668.jpg

  • 第二次bl是因?yàn)?code>while 循環(huán)。 即 只要是跳轉(zhuǎn),就會(huì)被hook,即有bl、b的指令,就會(huì)被hook

    2251862-b09a285f136e195d.jpg

?著作權(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)容