监控所有的OC方法耗时

前言

我的博客

看了戴铭大神App 启动优化与监控
,受益良多。我运用其中的hook objc_msgSend思想,写一个监控App里所有耗时的OC方法,以便以后开发过程中,能时刻监控App耗时性能问题。本文主要包含两方面:1、高性能hook objc_msgSend(我看了许多hook objc_msgSend,发现都没把性能做到极致。);2、把耗时OC方法的调用堆栈打印出来。

阅读建议

如果对arm64iOS ABI,还不是很了解,请看我前两篇文章。

源码

点击这里请在github上下载。

效果图

用法

把文件夹里的代码放到项目里,运行App时,摇一摇手机,就可以看到所有的OC方法耗时堆栈。

适用机型 (arm64的机型)

由于现在手机基本都是iPhone5s和更新的iPhone手机;而且性能问题本来就需要在真机上测试。因此只支持iPhone5s及更新的真机(arm64的iPad也适用),不适用模拟器

高性能hook objc_msgSend

源码

__attribute__((__naked__))
static void fake_objc_msgSend_safe()
{
    // backup registers
    __asm__ volatile(
                     "str x8,  [sp, #-16]!\n"  //arm64标准: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标准: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. 调用hook_objc_msgSend_before (保存lr和记录函数调用开始时间)
  3. 恢复寄存器。
  4. 调用objc_msgSend
  5. 保存寄存器。
  6. 调用hook_objc_msgSend_after (返回lr和函数结束时间减去开始时间,得到函数耗时)
  7. 恢复寄存器。
  8. ret。

为什么要用stack保存LR

  1. hook objc_msgSend里面调用了hook_objc_msgSend_before和hook_objc_msgSend_after函数,会覆盖LR寄存器,导致函数ret时候,不知道LR值,所以需要保存LR。
  2. objc_msgSend是可变参数函数,栈内存可能用到。所以也不能放栈内存里,只有构造一个stack。可保证函数的push和pop是一一对应的。
  3. 需要注意的是,保存LR的stack,每个线程都对应一个stack。(原因也是为了保证函数的push和pop是一一对应),所以引入了线程局部变量,pthread_setspecific(pthread_key_t , const void * _Nullable)和pthread_getspecific(pthread_key_t)函数,根据key,来设置和获取线程局部变量。

保存寄存器注意点

只需保存x0-x8,因为调用hook_objc_msgSend_before和hook_objc_msgSend_after,调用过程中可能会修改到这些寄存器。浮点数寄存器这两函数不会用到,不需要保存;x9等临时寄存器,不需要保存。

调用hook_objc_msgSend_before

由于函数hook_objc_msgSend_before(id self, SEL sel, uintptr_t lr),有三个参数,其中x0和x1已经存放self和SEL了,只需要设置第三个参数x2=lr。

调用hook_objc_msgSend_after

hook_objc_msgSend_after返回值是lr,返回值此时存放在x0里,所以lr=x0。

hook性能优化

  1. 由于App卡顿,绝大部分都是因为主线程卡顿造成,所以我们只需要监控主线程里运行的所有OC方法。但是hook objc_msgSend是hook所有的OC方法。网上很多hook方法都是把记录函数调用和保存LR放在一个stack里,最终调用hook_objc_msgSend_after时候,也只会统计主线程的耗时情况。

我用两个stack,一个专门存放LR值;另一个记录函数调用。避免子线程中OC方法的调用记录。

void hook_objc_msgSend_before(id self, SEL sel, uintptr_t lr)
{
    if (CallRecordEnable && pthread_main_np()) {
        //仅仅主线程记录函数调用
        pushCallRecord(object_getClass(self), sel);
    }
    //存放LR值
    setLRRegisterValue(lr);
}
  1. 支持设置记录的最大深度和最小耗时;超过这个深度和小于最小耗时的函数不记录。

记录OC方法耗时,需要记录的信息

typedef struct {
    Class cls;   //通过类可知道类名和方法是类方法还是实例方法(类是元类,说明是类方法)
    SEL sel;  //可知道方法名
    uint64_t costTime; //单位:纳秒(百万分之一秒)
    int depth;  
} TPCallRecord;
  1. x0中是self,通过self可以获得Class。
  2. x1中是sel
  3. 通过函数开始时间和结束时间,可以获得耗时
  4. 通过记录栈的深度,获得函数的深度。(注意:这里的深度是相对深度,因为我们仅记录部分OC方法的耗时)

把耗时OC方法的调用堆栈打印出来

获取的函数记录部分打印出来如下:

 深度      耗时            方法名
 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]
 .....

由于函数调用的栈是先进后出,根函数肯定是最后被记录,叶子函数最先被记录;并且同一层的函数,是先进先出。那我们如何还原成人更容易理解的函数调用堆栈呢?

  1. 第一步,从上往下,标记这个深度的记录,出现的次数。
深度 相同深度出现次数 耗时 方法名
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. 第二步,从下往上,从根函数开始,深度递增,出现次数相同的记录,挑选出来。得到:
 深度      耗时            方法名
  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 | 2.143ms |   -[OnlineSettingHelper4AppStore  all]
 1 | 2.527ms |  -[OnlineSettingHelper4AppStore  defaultUserAgent4SDWebImage]
 1 | 1.264ms |  +[SDWebImageManager  sharedManager]
 .....

得到:

 深度      耗时            方法名
  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]
 .....

结束语

这个工具我后面将持续更新,加入其它功能,更加方便开发过程中使用。假如它对你有益,不妨github上给个star~

引用和参考

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

--EOF-- 转载请保留链接,谢谢

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