iOS 性能監控(三)—— 方法耗時監控

前言:
最近,在看戴銘老師關於 「性能監控」 相關的技術分享,感受收穫不少。基於最近的學習,總結了一些性能監控相關的實踐,並計劃落地一系列 「性能監控」 相關的文章。

目錄以下:
iOS 性能監控(一)—— CPU功耗監控
iOS 性能監控(二)—— 主線程卡頓監控
iOS 性能監控(三)—— 方法耗時監控git


本篇將介紹iOS性能監控工具(QiLagMonitor)中與 「方法耗時監控」 相關的功能模塊。github

1、什麼是hook?

定義:hook是指在原有方法開始執行時,換成你指定的方法。或在原有方法的執行先後,添加執行你指定的方法。從而達到改變指定方法的目的。swift

例如:bash

  • 使用runtimeMethod Swizzle
  • 使用Facebook所開源的fishhook框架。

前者是ObjC運行時提供的「方法交換」能力。 後者是對Mach-O二進制文件的符號進行動態的「從新綁定」,已達到方法交換的目的。app

問題1: fishhook的大體實現思路是什麼?

《iOS App啓動優化(一)—— 瞭解App的啓動流程》中咱們提到,動態連接器dyld會根據Mach-O二進制可執行文件的符號表來綁定符號。而經過符號表及符號名就能夠知道指針訪問的地址,再經過更改指針訪問的地址就能替換指定的方法實現了。框架

問題2:爲何hook了objc_msgSend就能夠掌握全部objc方法的耗時?

由於objc_msgSend是全部Objective-C方法調用的必經之路,全部的Objective-C方法都會調用到運行時底層的objc_msgSend方法。因此只要咱們能夠hook objc_msgSend,咱們就能夠掌握全部objc方法的耗時。(更多詳情可看我以前寫的《iOS 編寫高質量Objective-C代碼(二)》的第六點 —— 理解objc_msgSend(對象的消息傳遞機制)ide

另外,objc_msgSend自己是用匯編語言寫的,蘋果已經開源了objc_msgSend的源碼。可在官網上下載查看:objc_msgSend源碼函數

2、如何hook底層objc_msgSend?

第一階段:與fishhook框架相似,咱們先要擁有hook的能力。
  • 首先,設計兩個結構體: 一個是用來記錄符號的結構體,一個是用來記錄符號表的鏈表。
struct rebinding {
    const char *name;
    void *replacement;
    void **replaced;
};

struct rebindings_entry {
    struct rebinding *rebindings;
    size_t rebindings_nel;
    struct rebindings_entry *next;
};
複製代碼
  • 其次,遍歷動態連接器dyld內全部的image,取出其中的headerslide。 以便咱們接下來拿到符號表。
static int fish_rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
    int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
    if (retval < 0) {
        return retval;
    }
    // If this was the first call, register callback for image additions (which is also invoked for
    // existing images, otherwise, just run on existing images
    //首先是遍歷 dyld 裏的全部的 image,取出 image header 和 slide。注意第一次調用時主要註冊 callback
    if (!_rebindings_head->next) {
        _dyld_register_func_for_add_image(_rebind_symbols_for_image);
    } else {
        uint32_t c = _dyld_image_count();
        // 遍歷全部dyld的image
        for (uint32_t i = 0; i < c; i++) {
            _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); // 讀取image內的header和slider
        }
    }
    return retval;
}
複製代碼
  • 上一步,咱們在dyld內拿到了全部image。 接下來,咱們從image內找到符號表內相關的segment_command_t,遍歷符號表找到所要替換的segname,再進行下一步方法替換。方法實現以下:
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
    Dl_info info;
    if (dladdr(header, &info) == 0) {
        return;
    }
    
    // 找到符號表相關的command,包括 linkedit_segment command、symtab command 和 dysymtab command。
    segment_command_t *cur_seg_cmd;
    segment_command_t *linkedit_segment = NULL;
    struct symtab_command* symtab_cmd = NULL;
    struct dysymtab_command* dysymtab_cmd = NULL;
    
    uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
        cur_seg_cmd = (segment_command_t *)cur;
        if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
                linkedit_segment = cur_seg_cmd;
            }
        } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
            symtab_cmd = (struct symtab_command*)cur_seg_cmd;
        } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
            dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
        }
    }
    
    if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
        !dysymtab_cmd->nindirectsyms) {
        return;
    }

    // 得到base符號表以及對應地址
    uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
    nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
    char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
    
    // 得到indirect符號表
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
    
    cur = (uintptr_t)header + sizeof(mach_header_t);
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
        cur_seg_cmd = (segment_command_t *)cur;
        if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
                strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
                continue;
            }
            for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
                section_t *sect =
                (section_t *)(cur + sizeof(segment_command_t)) + j;
                if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
                if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
            }
        }
    }
}
複製代碼
  • 最後,經過符號表以及咱們所要替換的方法的實現,進行指針地址替換。 這是相關方法實現:
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
                                           section_t *section,
                                           intptr_t slide,
                                           nlist_t *symtab,
                                           char *strtab,
                                           uint32_t *indirect_symtab) {
    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
    for (uint i = 0; i < section->size / sizeof(void *); i++) {
        uint32_t symtab_index = indirect_symbol_indices[i];
        if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
            symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
            continue;
        }
        uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
        char *symbol_name = strtab + strtab_offset;
        if (strnlen(symbol_name, 2) < 2) {
            continue;
        }
        struct rebindings_entry *cur = rebindings;
        while (cur) {
            for (uint j = 0; j < cur->rebindings_nel; j++) {
                if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                    if (cur->rebindings[j].replaced != NULL &&
                        indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
                        *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
                    }
                    indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
                    goto symbol_loop;
                }
            }
            cur = cur->next;
        }
    symbol_loop:;
    }
}
複製代碼

到這裏,經過調用下面的方法,咱們就擁有了hook的基本能力。工具

static int fish_rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
複製代碼
第二階段:經過彙編語言編寫出咱們的hook_objc_msgSend方法

由於objc_msgSend是經過彙編語言寫的,咱們想要替換objc_msgSend方法還須要從彙編語言下手。oop

既然咱們要作一個監控方法耗時的工具。這時想一想咱們的目的是什麼?

咱們的目的是:經過hookobjc_msgSend方法,在objc_msgSend方法前調用打點計時操做,在objc_msgSend方法調用後結束打點和計時操做。經過計算時間差,咱們就能精準的拿到方法調用的時長。

所以,咱們要在原有的objc_msgSend方法的調用先後須要加上before_objc_msgSendafter_objc_msgSend方法,以便咱們後期的打點計時操做。

arm64 有 31 個 64 bit 的整數型寄存器,分別用 x0 到 x30 表示。主要的實現思路是:

  • 入棧參數,參數寄存器是 x0~ x7。對於 objc_msgSend 方法來講,x0 第一個參數是傳入對象,x1 第二個參數是選擇器 _cmd。syscall 的 number 會放到 x8 裏。
  • 交換寄存器中保存的參數,將用於返回的寄存器 lr 中的數據移到 x1 裏。
  • 使用 bl label 語法調用 pushCallRecord 函數。
  • 執行原始的 objc_msgSend,保存返回值。
  • 使用 bl label 語法調用 popCallRecord 函數。
  • 返回

裏面涉及到的一些彙編指令:

指令 含義
stp 同時寫入兩個寄存器。
mov 將值賦值到一個寄存器。
ldp 同時讀取兩個寄存器。
sub 將兩個寄存器的值相減
add 將兩個寄存器的值相加
ret 從子程序返回主程序

詳細代碼以下:

#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");

#define save() \
__asm volatile ( \
"stp x8, x9, [sp, #-16]!\n" \
"stp x6, x7, [sp, #-16]!\n" \
"stp x4, x5, [sp, #-16]!\n" \
"stp x2, x3, [sp, #-16]!\n" \
"stp x0, x1, [sp, #-16]!\n");

#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n" );

#define link(b, value) \
__asm volatile ("stp x8, lr, [sp, #-16]!\n"); \
__asm volatile ("sub sp, sp, #16\n"); \
call(b, value); \
__asm volatile ("add sp, sp, #16\n"); \
__asm volatile ("ldp x8, lr, [sp], #16\n");

#define ret() __asm volatile ("ret\n");

__attribute__((__naked__))
static void hook_objc_msgSend() {
    // Save parameters.
    save() // stp入棧指令 入棧參數,參數寄存器是 x0~ x7。對於 objc_msgSend 方法來講,x0 第一個參數是傳入對象,x1 第二個參數是選擇器 _cmd。syscall 的 number 會放到 x8 裏。
    
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    
    // Call our before_objc_msgSend.
    call(blr, &before_objc_msgSend)
    
    // Load parameters.
    load()
    
    // Call through to the original objc_msgSend.
    call(blr, orig_objc_msgSend)
    
    // Save original objc_msgSend return value.
    save()
    
    // Call our after_objc_msgSend.
    call(blr, &after_objc_msgSend)
    
    // restore lr
    __asm volatile ("mov lr, x0\n");
    
    // Load original objc_msgSend return value.
    load()
    
    // return
    ret()
}
複製代碼

這時候,每當底層調用hook_objc_msgSend方法時,會先調用before_objc_msgSend方法,再調用hook_objc_msgSend方法,最後調用after_objc_msgSend方法。

單個方法調用,流程以下圖:

舉一反「三」,而後多層方法調用的流程,就變成了下圖:

這樣,咱們就能拿到每一層方法調用的耗時了。

3、如何使用這個工具?

第一步,在項目中,導入QiLagMonitor類庫。

第二步,在所須要監控的控制器中,導入QiCallTrace.h頭文件。

[QiCallTrace start]; // 1. 開始

  // your codes(你所要測試的代碼區間)

  [QiCallTrace stop]; // 2. 中止
  [QiCallTrace save]; // 3. 保存並打印方法調用棧以及具體方法耗時。
複製代碼

PS:目前該工具只能hook全部objc方法,並計算出區間內的全部方法耗時。暫不支持swift方法的監聽。

最後,本系列我是站在iOS業界巨人的肩膀上完成的,感謝戴銘老師精彩的技術分享。 祝你們學有所成,工做順利。
另附上,戴銘老師課程連接:《iOS開發高手課》,謝謝!

相關文章
相關標籤/搜索