經過objc_msgSend實現iOS方法耗時監控

前言

若是有這麼一個需求,要監聽全部OC方法的耗時時間,咱們要如何實現?本文將描述如何利用 fishhook 去攔截底層的 objc_msgSend 實現,從而達到監控全部 OC 方法的目的。html

Ps: 本文不涉及 objc_msgSend 源碼解析,只專一於 Hook及耗時統計。對其底層實現有興趣的能夠查看 arm64下Objc_msgSend的實現註釋 ,已詳細註釋了每句彙編代碼。git

基礎知識

在開始以前,須要先了解些基礎知識,有便於後續的閱讀。github

Fishhook

fishhook是facebook開源的老牌Hook框架,相比於利用消息轉發機制實現 Method Swizzle,其在 dylib 連接 Mach-O 時,更改動態綁定地址,能實現系統C/C++函數的Hook,具體可查看筆者以前寫的 Hook利器之fishhookobjective-c

arm64基礎知識

因爲 Objc_msgSend 使用匯編實現,因此咱們須要學習些會用到的彙編指令,才能清晰的知道每一步在作什麼。編程

arm64 有64位處理器,能同時處理64位數據,其每條指令固定長度爲32bit,即4個字節。api

arm64 有34個寄存器,其中包括31個通用寄存器、SP、PC、CPSR。

  • x0-x30數據結構

    一般 x0 – x7 分別會存放方法的前 8 個參數,若是參數個數超過了8個,多餘的參數會存在棧上,新方法會經過棧來讀取。框架

    當方法有返回值時,執行結束會將結果放在 x0 上,若是方法返回值是一個較大的數據結構時,結果則會存在 x8 執行的地址上。編程語言

    x29 又稱爲FP,用於保存棧底地址。函數

    x30 寄存器又稱爲LR,用於保存要執行的下一條指令,後面咱們 Hook 時須要屢次存取該寄存器。

    w0-w30 表示訪問其低32位寄存器

  • SP(x30)

    棧寄存器,在任意時刻會保存咱們棧頂的地址,後面咱們會經過偏移它來暫存參數。

  • PC(x31)

    存放當前執行的指令的地址,不可被修改。

  • SPRs

    SPRs是狀態寄存器,用於存放程序運行中一些狀態標識。不一樣於編程語言裏面的if else.在彙編中就須要根據狀態寄存器中的一些狀態來控制分支的執行。狀態寄存器又分爲 The Current Program Status Register (CPSR) 和 The Saved Program Status Registers (SPSRs)。 通常都是使用 CPSR, 當發生異常時, CPSR 會存入 SPSR 。當異常恢復,再拷貝 CPSR。瞭解便可。

會用到的彙編指令

指令 做用
mov 用寄存器之間的賦值
str、stp 存儲指令,用於將一個/一對寄存器的數據寫入內存中
ldr、ldp 加載指令,用於從內存讀取一個/一對數據到寄存器
b、bl、blr 跳轉指令,分別是不帶返回、帶返回、帶返回並指定pc
ret 子程序返回指令,返回地址默認保存在LR(X30)

另外還有ADD、SUB、AND、CBZ等指令,在objc_msgSend源碼裏會用到,這裏不涉及就不具體闡述了,有興趣的可搜下arm64指令集。

在Xcode編寫彙編須要用到 GCC內嵌彙編,有興趣能夠看下 ARM GCC內嵌, 僅作了解。

設計流程

設計思路: 當咱們要統計函數的耗時,最直接的方式就是記錄執行函數前和執行函數後的時間,差值就是所消耗時間。

  • 使用 fishihook 對 objc_msgSend 進行替換,實現本身的 hook_objc_msgSend
  • 經過彙編代碼將調用時寄存器中的參數保存和恢復
  • 實現調用先後的計時方法 before_objc_msgSend 和 after_objc_msgSend

流程

因爲存在多層級 objc_msgSend 調用,因此須要涉及一個調用棧來保存調用層級和起始時間。調用before_objc_msgSend時,都將最新的調用指令進行入棧操做,記錄當前時間和調用層級,調用after_objc_msgSend時取出棧頂元素,便可獲得方法及對應的耗時。

具體代碼

設計調用棧及初始化變量

//用於記錄方法
typedef struct {
    Class cls;
    SEL sel;
    uint64_t time;
    uintptr_t lr; // lr寄存器內的地址值
} MethodRecord;

//主線程方法調用棧
typedef struct {
    MethodRecord *stack;
    int allocCount; //棧內最大可放元素個數
    int index; //當前方法的層級
    bool isMainThread;
} ThreadMethodStack;


static id (*orgin_objc_msgSend)(id, SEL, ...);
static pthread_key_t threadStackKey;

static YEThreadCallRecord *threadCallRecords = NULL;
static int recordsCurrentCount;
static int recordsAllocCount;

static int maxDepth = 3; // 設置記錄的最大層級
static uint64_t minConsumeTime = 1000; // 設置記錄的最小耗時


複製代碼

定義兩個結構體來構成咱們的調用棧。

對 objc_msgSend 進行攔截

void startMonitor(void) {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        pthread_key_create(&threadStackKey, cleanThreadStack);
        struct rebinding rebindObjc_msgSend;
        rebindObjc_msgSend.name = "objc_msgSend";
        rebindObjc_msgSend.replacement = hook_objc_msgSend;
        rebindObjc_msgSend.replaced = (void *)&orgin_objc_msgSend;
        struct rebinding rebs[1] = {rebindObjc_msgSend};
        rebind_symbols(rebs, 1);
    });
}

// 線程私有數據的清理函數
void cleanThreadStack(void *ptr) {
    if (ptr != NULL) {
        ThreadMethodStack *threadStack = (ThreadMethodStack *)ptr;
        if (threadStack->stack) {
            free(threadStack->stack);
        }
        free(threadStack);
    }
}

// 獲取當前線程的調用棧
ThreadMethodStack* getThreadMethodStack() {
    
    ThreadMethodStack *tms = (ThreadMethodStack *)pthread_getspecific(threadStackKey);
    if (tms == NULL) {
        tms = (ThreadMethodStack *)malloc(sizeof(ThreadMethodStack));
        tms->stack = (MethodRecord *)calloc(128, sizeof(MethodRecord));
        tms->allocLength = 64;
        tms->index = -1;
        tms->isMainThread = pthread_main_np();
        pthread_setspecific(threadStackKey, tms);
    }
    return tms;
}
複製代碼

因爲 objc_msgSend 會在多個線程被調用,因此須要讓保證當前線程的調用棧不被其餘線程修改,這時就用到了 pthread_key_create 線程私有數據概念,關於這部份內容可查看筆者以前的文章 什麼是線程私有數據

利用 fishhook 提供的 api 將 objc_msgSend 替換成咱們的 hook_objc_msgSend, 並建立一個key爲 _thread_key 線程私有數據方便後續存放調用棧。

實現自定義 hook_objc_msgSend

void before_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
    ThreadMethodStack *tms = getThreadMethodStack();
    if (tms) {
        int nextIndex = (++tms->index);
        if (nextIndex >= tms->allocCount) {
            tms->allocCount += 64;
            tms->stack = (MethodRecord *)realloc(tms->stack, tms->allocCount * sizeof(MethodRecord));
        }
        MethodRecord *newRecord = &tms->stack[nextIndex];
        newRecord->cls = object_getClass(self);
        newRecord->sel = _cmd;
        newRecord->lr = lr;
        if (tms->isMainThread) {
            struct timeval now;
            gettimeofday(&now, NULL);
            newRecord->time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
        }
    }
}

uintptr_t after_objc_msgSend() {
    ThreadMethodStack *tms = getThreadMethodStack();
    int curIndex = tms->index;
    int nextIndex = tms->index--;
    MethodRecord *record = &tms->stack[nextIndex];
    
    if (tms->isMainThread) {
        struct timeval now;
        gettimeofday(&now, NULL);
        uint64_t time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
        if (time < record->time) {
            time += 100 * 1000000;
        }
        uint64_t cost = time - record->time;
        if (cost > minConsumeTime && tms->index < maxDepth) {
            // 爲記錄棧分配內存
            if (!threadCallRecords) {
                recordsAllocCount = 1024;
                threadCallRecords = malloc(sizeof(threadCallRecords) * recordsAllocCount);
            }
            recordsCurrentCount++;
            if (recordsCurrentCount >= recordsAllocCount) {
                recordsAllocCount += 1024;
                threadCallRecords = realloc(threadCallRecords, sizeof(threadCallRecords) * recordsAllocCount);
            }
            // 添加記錄元素
            YEThreadCallRecord *yeRecord = &threadCallRecords[recordsCurrentCount - 1];
            yeRecord->cls = record->cls;
            yeRecord->depth = curIndex;
            yeRecord->sel = record->sel;
            yeRecord->time = cost;
        }
    }
    // 恢復下條指令
    return record->lr;
}

 //arm64標準:sp % 16 必須等於0
#define saveParameters() \ __asm volatile ( \ "str x8, [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 loadParameters() \ __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" \ "ldr x8, [sp], #16\n" );

// 前置方法的返回值會儲存在x8上,因此調用咱們本身的方法前先保存下x8,接着將方法加載到x12中去執行指令
#define call(b, value) \ __asm volatile ("str x8, [sp, #-16]!\n"); \ __asm volatile ("mov x12, %0\n" :: "r"(value)); \ __asm volatile ("ldr x8, [sp], #16\n"); \ __asm volatile (#b " x12\n");

// 替換的objc_msgSend
__attribute__((__naked__))
static void hook_objc_msgSend() {
    // 存原調用參數
    saveParameters()
    
    // 將lr存放的指令放在x2,做爲before_objc_msgSend參數
    __asm volatile ("mov x2, lr\n");

    // Call our before_objc_msgSend.
    call(blr, &before_objc_msgSend)
    
    // 恢復參數
    loadParameters()
    
    // 調用objc_msgSend
    call(blr, orgin_objc_msgSend)
    

    saveParameters()
    
    // Call our after_objc_msgSend.
    call(blr, &after_objc_msgSend)
    
    // x0存的是after_objc_msgSend返回的下條指令,返回給lr指針
    __asm volatile ("mov lr, x0\n");
    
    // Load original objc_msgSend return value.
    loadParameters()
    
    // 返回
    __asm volatile ( "ret");
}
複製代碼

實現比較簡單,將寄存器x0至x8的內容保存到棧內存中,調用 before_objc_msgSend 建立當前方法對應結構體 MethodRecord ,記錄當前時間、方法信息並保存 lr 指針後,恢復x0至x8的內容調用原 objc_msgSend;調用結束後再次重複存取寄存器內容,將 MethodRecord 取出計算總體耗時。

須要注意的是,當咱們在 objc_msgSend 的過程當中調用自定義的函數時,會改變 lr 寄存器中的值,致使最後的 ret函數 找不到下一條指令,因此須要在 before_objc_msgSend 記錄 lr值,並在 after_objc_msgSend 恢復。

拓展

經過Aspects實現耗時監控

在寫本文時,初期嘗試過用 Aspects 框架實現耗時監控,這裏列出當時的思考。

  • 實現方式
    1. 遍歷須要Hook的類
      1. objc_copyClassNamesForImage 獲取開發者建立的類
      2. objc_getClassList 獲取全部類
      3. 手動注入要Hook的類
    2. 獲取每一個類的方法( class_copyMethodList )
    3. 經過Aspects先後插樁獲取時間計算差值
  • 一些問題
    • 因爲不知道調用順序,實現記錄調用層級困難
    • 類和方法較多時hook時間較久
    • 相比於彙編實現,性能較差

參考連接

iOS開發高手課-02 App啓動速度怎麼作優化與監控

OC方法監控

About Me 🐝

今年計劃完成10個優秀第三方源碼解讀,會陸續提交到 iOS-Framework-Analysis ,歡迎 star 項目陪伴筆者一塊兒提升進步,如有什麼不足之處,敬請告知 🏆。

相關文章
相關標籤/搜索