啟動優化-二進制重排與clang插樁

二進制重排原理

啟動優化-概念與建議

  • 在上一篇啟動優化的概念中,我們理解了虛擬內存與物理內存,在加載APP不活躍的部分時,會訪問虛擬內存的page(映射表),而對應的物理內存沒有與映射表與之關聯的話,將會觸發缺頁異常(page fault),這個時候就必須將應用在虛擬內存不活躍的部分在映射表進行與物理內存的關聯再加載應用到物理內存,可以理解觸發缺頁異常會對性能有一定的影響性。

  • 一般來說,App在冷啟動的過程中,會有很多的類,分類,第三方需要加載和執行,也就是會觸發最多次的缺頁異常,當許多的缺頁異常一起觸發會帶來大量的耗時,如下以WeChat為例,啟動階段觸發Page Fault的次數

  • 打開instrumentsSystem Trace

  • 點擊啟動,為了表現冷啟動,需要重啟手機清除緩存數據,
  • 從instruments測試結果,可以看到pagefault次數有3193次,可以看到這個是非常影響性能的

優化思路

測試

  • 首先我們通過以下Demo查看方法在編譯時期的排列順序,在viewController中按下列順序定義,以下幾個方法
   @implementation ViewController

   void test1(){
       printf("1");
   }

   void test2(){
       printf("2");
   }

   - (void)viewDidLoad {
       [super viewDidLoad];

       test1();
   }

   +(void)load{
       printf("3");
       test2();
   }
   @end

  • Build Setting → Write Link Map File 設置為YES
  • linkmap路徑 (或是上一張圖片的path to Link Map File)。
  • CMD+B編譯demo,依據對應的路徑查找link map文件,如下所示,可以發現類中函數的加載順序是從上到下的。
*   而文件順序是根據Build Phases -> Compile Sources中的順序加載的。

思路

由上面的測試,可以看到,文件加載以及函數的調用的順序會影響page fault的數量,有極大的可能,在啟動時刻調用的方法是不同的page的,所以我們可以將我們在啟動時刻調用的方法排列在同一頁中,如此一來就可以減少觸發page fault的次數,這也就是二進制重排原理。

  • 注意:在iOS生產環境的app,在發生page fault進行重新加載時,iOS系統還會對其做一次簽名驗證 ,因此iOS在生產環境的page Fault比Debug環境下所產生的耗時更多時間。

實現二進制重排

名詞解釋

Link Map

  • Linkmap是iOS編譯過程中間的產物,紀錄了二進制的文件佈局,需要再Xcode的Build Settings裡開啟Write Link Map File , Link Map 主要包含三個部分:
    • Object Files生成二進制用到的link單元的路徑和文件編號
    • Sections 紀錄Mach-O每個Segment/section的地址範圍
    • Symbols按順序紀錄每個符號的地址範圍

ld

  • ld是Xcode使用的鏈接器,有一個參數order_file,我們可以通過在Build Settings -> Order File 配置一個後綴為order的文件路徑。在這個order文件中,將所需要的符號按照順序寫在裡面,在項目編譯時,會按照這個文件的順序進行加載,以此來達到我們的優化。也就是說二進制重排的本質就是對啟動加載的符號進行重新排列。
  • 但是我們要如何獲取我們的函數呢?以下有幾個思路...
    1. hook objc_msgSend:我們知道,函數的本質是發送消息,在底層都會來到objc_msgSend,但是由於objc_msgSend的參數是可變的,需要通過彙編獲取,對開發人員要求較高,且也只能拿到OC和swift中@objc後的方法。
    2. 靜態掃描:掃描Mach-O特定段ㄉ和節裡面所存儲的符號以及函數數據
    3. Clang插樁:批量hook,可以實現100%符號覆蓋,最完全獲取swift,OC,block函數

Clang 插樁

  • llvm內置了一個簡單的代碼覆蓋率檢測(SanitizerCoverage)。他對於基本塊級邊緣級插入對用戶定義函數的調用。接下來介紹的批量hook,就需要借助於sanitizerCoverage
  • 關於clang的插樁覆蓋的官方文檔如下:

Clang 12 documentation

  • 文檔中有詳細描述即簡短的Demo演示。

第一步:開啟SanitizerCoverage

  • OC項目:Build SettingsOther C Flags 添加 fsanitize-coverage=func,trace-pc-guard
  • Swift項目:須額外在Build SettingsOther Swift Flags 中加入sanitize-coverage=funcsanitize=undefined
  • 所有鏈接到App中的二進制都需要開啟SanitizerCoverage ,這樣才能完全覆蓋到所有調用
  • 也可以通過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

第二步:重寫方法,新建一個OC文件ACOrderFile,重寫兩個方法

  • __sanitizer_cov_trace_pc_guard_init 方法

    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.
    }
    
    
    • 參數1:start是一個指針,指向無符號int類,4個字節,相當於一個數組的起始位置 ,即符號的起始位置(是從高位往低位讀)
  • 參數2:Stop 也是一個指針,由於取數據是從高地址往低地址讀取的,所以如果直接讀取stop的地址並不是stop真正的地址,而是讀取到最後的地址,所以讀取stop時,由於stop占4個字節,stop真實地址 = stop打印的地址-0x4
  • stop內存地址中存儲的值表示什麼呢?我們可以增加一個方法/C++/屬性的方法(多3個),發現其值也會增加對應的數,例如我們增加一個test1方法。可以看到數據從1d變成了1e
  • __sanitizer_cov_trace_pc_guard 方法,主要是捕獲所有的啟動時刻的符號,將所有符號入隊
  • 參數guard是一個哨兵,告訴我們是第幾個被調用的
  • 符號的存儲需要借助於鏈表,所以需要定義鏈表節點ACNode
  • 通過OSQueueHead創建原子隊列,其目的是保證讀寫的安全
  • 通過OSAtomicEnqueue 方法將node入隊,通過鏈表的next指針可以訪問下一個符號
       //原子隊列,其目的是保證寫入安全,線程安全
       static  OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
       //定義符號結構體,以鏈表的形式
       typedef struct {
           void *pc;
           void *next;
       }ACNode;

       /*
        - start:起始位置
        - stop:並不是最後一個符號地址,而是整个符號表的最後一个地址,
        最後一个符號的地址=stop-4
       (因為讀取數據是從高地址往低地址讀取的,且stop是一个無符號int類型,占4个字節)。
        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) {
           //将load方法过滤掉了,所以需要注释掉
           //    if (!*guard) return;

           //獲取PC
           /*
            - PC 當前函数返回上一個调用的地址
            - 0 當前這個函数地址,即當前函数的返回地址
            - 1 當前函数调用者的地址,即上一個函数的返回地址
           */
           void *PC = __builtin_return_address(0);
           //創建node,並赋值
           ACNode *node = malloc(sizeof(ACNode));
           *node = (ACNode){PC, NULL};

           //加入隊列
           //符号的訪問不是通過下標訪問,是通過链表的next指針,
           //所以需要借用offsetof(結構體類型,下一個的地址即next)
           OSAtomicEnqueue(&queue, node, offsetof(ACNode, next));
       }

第三步:獲取所有符號並寫入文件

  • while循環從隊列中取出符號,處理非OC方法的前綴,存入數組中
  • 數組取反,因為入隊儲存的順序是反序的。
  • 數組去重,並移除本身方法的符號
  • 將數組中的符號轉成字符串並寫入到AC.oder文件中
  • 另外也可以在第一個didFinishLaunchingWithOptions根視圖,獲取符號(這部分可以由自己決定,以下範例是在touchesBegan方法調用時獲取)
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
        //while 循環取符號
        while (YES) {
            //出隊
            ACNode * node = OSAtomicDequeue(&symbolList, offsetof(ACNode, next));
            if (node == NULL) {
                break;
            }
            //取出PC存入info
            Dl_info info;
            dladdr(node->pc, &info);
            //printf("%s \\n", info.dli_sname);
            NSString * name = @(info.dli_sname);
            //判斷是不是OC方法,如果不是需要加下滑線,反之,則直接存儲
            BOOL  isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
            NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
            [symbolNames addObject:symbolName];
        }
        //取反 (隊列的存儲是反序的)
        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:[NSString stringWithFormat:@"%s",__FUNCTION__]];
        //將數組變成字符串
        NSString * funcStr = [funcs  componentsJoinedByString:@"\\n"];
        //字符串寫入文件
        NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AC.order"];
        NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
        [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
        NSLog(@"%@",funcStr);
    }

注意點:避免死循環

  • Build Settings -> Other C Flags 的如果配置的是-fsanitize-coverage=trace-pc-guard ,在while部分會出現死循環(我們在touchBegin方法中調適)
  • 我們透過彙編查看,發現有三個地方調用__sanitizer_cov_trace_pc_guard 的調用
  • 第一次 touchBegin調用__sanitizer_cov_trace_pc_guard
  • 第二次bl跳轉跳轉因為while循環,只要跳轉就會被hook,即有b,bl指令,就會被hook
  • 第三次bl是printf

解決方式是將BuildSetting中的other C Flags改成-fsanitize-coverage=func,trace-pc-guard

第四步 拷貝文件,放入指定位置 並配置路徑

  • 一般將該文件放入主項目的路徑下,並在Build Settings -> Order File 中配置./AC.order,下面是配置前後的對比
  • 沒有配置,照序加載
  • 有配置,依照啟動時時刻所需(自己配置的.oder文件)加載


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容