3

监控所有的OC方法耗时

 2 years ago
source link: https://wukaikai.tech/2019/06/27/%E7%9B%91%E6%8E%A7%E6%89%80%E6%9C%89%E7%9A%84OC%E6%96%B9%E6%B3%95%E8%80%97%E6%97%B6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

监控所有的OC方法耗时

发表于 2019-06-27

  1. 1.1版本:增加耗时方法排序功能和耗时方法中调用次数排序功能(已做)
  1. 1.2版本:优化代码质量和性能问题(未做)
  2. 1.3版本:增加打印卡顿时候,所有线程堆栈 (未做)

欢迎大家关注我的掘金主页,我后面会以掘金为主;有文章先发在掘金上,然后同步到自己博客上来。

看了戴铭大神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. 恢复寄存器。

为什么要用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~

引用和参考

–EOF– 若无特别说明,本站文章均为原创,转载请保留链接,谢谢


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK