監(jiān)控所有的OC方法耗時(shí)

前言

我的博客

看了戴銘大神App 啟動(dòng)優(yōu)化與監(jiān)控
,受益良多。我運(yùn)用其中的hook objc_msgSend思想,寫(xiě)一個(gè)監(jiān)控App里所有耗時(shí)的OC方法,以便以后開(kāi)發(fā)過(guò)程中,能時(shí)刻監(jiān)控App耗時(shí)性能問(wèn)題。本文主要包含兩方面:1、高性能hook objc_msgSend(我看了許多hook objc_msgSend,發(fā)現(xiàn)都沒(méi)把性能做到極致。);2、把耗時(shí)OC方法的調(diào)用堆棧打印出來(lái)。

閱讀建議

如果對(duì)arm64iOS ABI,還不是很了解,請(qǐng)看我前兩篇文章。

源碼

點(diǎn)擊這里請(qǐng)?jiān)趃ithub上下載。

效果圖

用法

把文件夾里的代碼放到項(xiàng)目里,運(yùn)行App時(shí),搖一搖手機(jī),就可以看到所有的OC方法耗時(shí)堆棧。

適用機(jī)型 (arm64的機(jī)型)

由于現(xiàn)在手機(jī)基本都是iPhone5s和更新的iPhone手機(jī);而且性能問(wèn)題本來(lái)就需要在真機(jī)上測(cè)試。因此只支持iPhone5s及更新的真機(jī)(arm64的iPad也適用),不適用模擬器,

高性能hook objc_msgSend

源碼

__attribute__((__naked__))
static void fake_objc_msgSend_safe()
{
    // backup registers
    __asm__ volatile(
                     "str x8,  [sp, #-16]!\n"  //arm64標(biāo)準(zhǔn):sp % 16 必須等于0
                     "stp x6, x7, [sp, #-16]!\n"
                     "stp x4, x5, [sp, #-16]!\n"
                     "stp x2, x3, [sp, #-16]!\n"
                     "stp x0, x1, [sp, #-16]!\n"
                     );
    // prepare args and call func
    __asm volatile (
                    /*
                     hook_objc_msgSend_before(id self, SEL sel, uintptr_t lr)
                     x0=self  x1=sel x2=lr
                     */
                    "mov x2, lr\n"
                    "bl _hook_objc_msgSend_before"
                    );
    
    // restore registers
    __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"
                    );
    
    call(blr, orgin_objc_msgSend)

    // backup registers
    __asm__ volatile(
                     "str x8,  [sp, #-16]!\n"  //arm64標(biāo)準(zhǔn):sp % 16 必須等于0
                     "stp x6, x7, [sp, #-16]!\n"
                     "stp x4, x5, [sp, #-16]!\n"
                     "stp x2, x3, [sp, #-16]!\n"
                     "stp x0, x1, [sp, #-16]!\n"
                     );
    
    __asm volatile (
                    "bl _hook_objc_msgSend_after"
                    );
    
    __asm volatile (
                    "mov lr, x0\n"
                    );
    
    // restore registers
    __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"
                    );

    __asm volatile ("ret");
}

hook基本步驟

  1. 保存寄存器。
  2. 調(diào)用hook_objc_msgSend_before (保存lr和記錄函數(shù)調(diào)用開(kāi)始時(shí)間)
  3. 恢復(fù)寄存器。
  4. 調(diào)用objc_msgSend
  5. 保存寄存器。
  6. 調(diào)用hook_objc_msgSend_after (返回lr和函數(shù)結(jié)束時(shí)間減去開(kāi)始時(shí)間,得到函數(shù)耗時(shí))
  7. 恢復(fù)寄存器。
  8. ret。

為什么要用stack保存LR

  1. hook objc_msgSend里面調(diào)用了hook_objc_msgSend_before和hook_objc_msgSend_after函數(shù),會(huì)覆蓋LR寄存器,導(dǎo)致函數(shù)ret時(shí)候,不知道LR值,所以需要保存LR。
  2. objc_msgSend是可變參數(shù)函數(shù),棧內(nèi)存可能用到。所以也不能放棧內(nèi)存里,只有構(gòu)造一個(gè)stack。可保證函數(shù)的push和pop是一一對(duì)應(yīng)的。
  3. 需要注意的是,保存LR的stack,每個(gè)線(xiàn)程都對(duì)應(yīng)一個(gè)stack。(原因也是為了保證函數(shù)的push和pop是一一對(duì)應(yīng)),所以引入了線(xiàn)程局部變量,pthread_setspecific(pthread_key_t , const void * _Nullable)和pthread_getspecific(pthread_key_t)函數(shù),根據(jù)key,來(lái)設(shè)置和獲取線(xiàn)程局部變量。

保存寄存器注意點(diǎn)

只需保存x0-x8,因?yàn)檎{(diào)用hook_objc_msgSend_before和hook_objc_msgSend_after,調(diào)用過(guò)程中可能會(huì)修改到這些寄存器。浮點(diǎn)數(shù)寄存器這兩函數(shù)不會(huì)用到,不需要保存;x9等臨時(shí)寄存器,不需要保存。

調(diào)用hook_objc_msgSend_before

由于函數(shù)hook_objc_msgSend_before(id self, SEL sel, uintptr_t lr),有三個(gè)參數(shù),其中x0和x1已經(jīng)存放self和SEL了,只需要設(shè)置第三個(gè)參數(shù)x2=lr。

調(diào)用hook_objc_msgSend_after

hook_objc_msgSend_after返回值是lr,返回值此時(shí)存放在x0里,所以lr=x0。

hook性能優(yōu)化

  1. 由于App卡頓,絕大部分都是因?yàn)橹骶€(xiàn)程卡頓造成,所以我們只需要監(jiān)控主線(xiàn)程里運(yùn)行的所有OC方法。但是hook objc_msgSend是hook所有的OC方法。網(wǎng)上很多hook方法都是把記錄函數(shù)調(diào)用和保存LR放在一個(gè)stack里,最終調(diào)用hook_objc_msgSend_after時(shí)候,也只會(huì)統(tǒng)計(jì)主線(xiàn)程的耗時(shí)情況。

我用兩個(gè)stack,一個(gè)專(zhuān)門(mén)存放LR值;另一個(gè)記錄函數(shù)調(diào)用。避免子線(xiàn)程中OC方法的調(diào)用記錄。

void hook_objc_msgSend_before(id self, SEL sel, uintptr_t lr)
{
    if (CallRecordEnable && pthread_main_np()) {
        //僅僅主線(xiàn)程記錄函數(shù)調(diào)用
        pushCallRecord(object_getClass(self), sel);
    }
    //存放LR值
    setLRRegisterValue(lr);
}
  1. 支持設(shè)置記錄的最大深度和最小耗時(shí);超過(guò)這個(gè)深度和小于最小耗時(shí)的函數(shù)不記錄。

記錄OC方法耗時(shí),需要記錄的信息

typedef struct {
    Class cls;   //通過(guò)類(lèi)可知道類(lèi)名和方法是類(lèi)方法還是實(shí)例方法(類(lèi)是元類(lèi),說(shuō)明是類(lèi)方法)
    SEL sel;  //可知道方法名
    uint64_t costTime; //單位:納秒(百萬(wàn)分之一秒)
    int depth;  
} TPCallRecord;
  1. x0中是self,通過(guò)self可以獲得Class。
  2. x1中是sel
  3. 通過(guò)函數(shù)開(kāi)始時(shí)間和結(jié)束時(shí)間,可以獲得耗時(shí)
  4. 通過(guò)記錄棧的深度,獲得函數(shù)的深度。(注意:這里的深度是相對(duì)深度,因?yàn)槲覀儍H記錄部分OC方法的耗時(shí))

把耗時(shí)OC方法的調(diào)用堆棧打印出來(lái)

獲取的函數(shù)記錄部分打印出來(lái)如下:

 深度      耗時(shí)            方法名
 4 | 6.361ms |     +[Utility  isPbPackage]
 3 | 6.782ms |    -[SharedLib  implIsJailBrokenIPA]
 2 |   6.8ms |   -[SharedLib  isJailBrokenIPA]
 1 | 7.765ms |  +[OnlineSettingHelper  sharedInstance]
 2 | 2.143ms |   -[OnlineSettingHelper4AppStore  all]
 1 | 2.527ms |  -[OnlineSettingHelper4AppStore  defaultUserAgent4SDWebImage]
 1 | 1.264ms |  +[SDWebImageManager  sharedManager]
 0 | 11.56ms | -[AppDelegate  setUAForSDWebImageView]
 .....

由于函數(shù)調(diào)用的棧是先進(jìn)后出,根函數(shù)肯定是最后被記錄,葉子函數(shù)最先被記錄;并且同一層的函數(shù),是先進(jìn)先出。那我們?nèi)绾芜€原成人更容易理解的函數(shù)調(diào)用堆棧呢?

  1. 第一步,從上往下,標(biāo)記這個(gè)深度的記錄,出現(xiàn)的次數(shù)。
深度 相同深度出現(xiàn)次數(shù) 耗時(shí) 方法名
4 1 ... +[Utility  isPbPackage]
3 1 ... -[SharedLib  implIsJailBrokenIPA]
2 1 ... -[SharedLib  isJailBrokenIPA]
1 1 ... +[OnlineSettingHelper  sharedInstance]
2 2 ... -[OnlineSettingHelper4AppStore  all]
1 2 ... -[OnlineSettingHelper4AppStore  default...
1 3 ... +[SDWebImageManager  sharedManager]
0 1 ... -[AppDelegate  setUAForSDWebImageView]
  1. 第二步,從下往上,從根函數(shù)開(kāi)始,深度遞增,出現(xiàn)次數(shù)相同的記錄,挑選出來(lái)。得到:
 深度      耗時(shí)            方法名
  0 | 11.56ms | -[AppDelegate  setUAForSDWebImageView]
  1 | 7.765ms |  +[OnlineSettingHelper  sharedInstance]
  2 |   6.8ms |   -[SharedLib  isJailBrokenIPA]
  3 | 6.782ms |    -[SharedLib  implIsJailBrokenIPA]
 4 | 6.361ms |     +[Utility  isPbPackage]
 .....
  1. 第三步,從最上面一個(gè)沒(méi)有挑選的記錄區(qū)域(挑選的記錄,把整個(gè)記錄分割成多個(gè)未選擇的區(qū)域。),遞歸第二步。這個(gè)例子比較特殊,只有剩下一個(gè)未選擇的區(qū)域(如果中間被選擇了,那就分成多個(gè)區(qū)域)如下:
 深度      耗時(shí)            方法名
 2 | 2.143ms |   -[OnlineSettingHelper4AppStore  all]
 1 | 2.527ms |  -[OnlineSettingHelper4AppStore  defaultUserAgent4SDWebImage]
 1 | 1.264ms |  +[SDWebImageManager  sharedManager]
 .....

得到:

 深度      耗時(shí)            方法名
  0 | 11.56ms | -[AppDelegate  setUAForSDWebImageView]
  1 | 7.765ms |  +[OnlineSettingHelper  sharedInstance]
  2 |   6.8ms |   -[SharedLib  isJailBrokenIPA]
  3 | 6.782ms |    -[SharedLib  implIsJailBrokenIPA]
 4 | 6.361ms |     +[Utility  isPbPackage]
 1 | 2.527ms |  -[OnlineSettingHelper4AppStore  defaultUserAgent4SDWebImage]
 2 | 2.143ms |   -[OnlineSettingHelper4AppStore  all]
 1 | 1.264ms |  +[SDWebImageManager  sharedManager]
 .....

結(jié)束語(yǔ)

這個(gè)工具我后面將持續(xù)更新,加入其它功能,更加方便開(kāi)發(fā)過(guò)程中使用。假如它對(duì)你有益,不妨github上給個(gè)star~

引用和參考

  1. https://time.geekbang.org/column/article/85331
  2. https://github.com/facebook/fishhook

--EOF-- 轉(zhuǎn)載請(qǐng)保留鏈接,謝謝

最后編輯于
?著作權(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)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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