内容简介:看了戴铭大神App 启动优化与监控,受益良多。我运用其中的hook objc_msgSend思想,写一个监控App里所有耗时的OC方法,以便以后开发过程中,能时刻监控App耗时性能问题。本文主要包含两方面:1、高性能hook objc_msgSend(我看了许多hook objc_msgSend,发现都没把性能做到极致。);2、把耗时OC方法的调用堆栈打印出来。如果对arm64和iOS ABI,还不是很了解,请看我前两篇文章。
看了戴铭大神App 启动优化与监控,受益良多。我运用其中的hook objc_msgSend思想,写一个监控App里所有耗时的OC方法,以便以后开发过程中,能时刻监控App耗时性能问题。本文主要包含两方面:1、高性能hook objc_msgSend(我看了许多hook objc_msgSend,发现都没把性能做到极致。);2、把耗时OC方法的调用堆栈打印出来。
阅读建议
如果对arm64和iOS 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基本步骤
- 保存寄存器。
- 调用hook_objc_msgSend_before (保存lr和记录函数调用开始时间)
- 恢复寄存器。
- 调用objc_msgSend
- 保存寄存器。
- 调用hook_objc_msgSend_after (返回lr和函数结束时间减去开始时间,得到函数耗时)
- 恢复寄存器。
- ret。
为什么要用stack保存LR
- hook objc_msgSend里面调用了hook_objc_msgSend_before和hook_objc_msgSend_after函数,会覆盖LR寄存器,导致函数ret时候,不知道LR值,所以需要保存LR。
- objc_msgSend是可变参数函数,栈内存可能用到。所以也不能放栈内存里,只有构造一个stack。可保证函数的push和pop是一一对应的。
- 需要注意的是,保存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性能优化
- 由于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); } 复制代码
- 支持设置记录的最大深度和最小耗时;超过这个深度和小于最小耗时的函数不记录。
记录OC方法耗时,需要记录的信息
typedef struct { Class cls; //通过类可知道类名和方法是类方法还是实例方法(类是元类,说明是类方法) SEL sel; //可知道方法名 uint64_t costTime; //单位:纳秒(百万分之一秒) int depth; } TPCallRecord; 复制代码
- x0中是self,通过self可以获得Class。
- x1中是sel
- 通过函数开始时间和结束时间,可以获得耗时
- 通过记录栈的深度,获得函数的深度。(注意:这里的深度是相对深度,因为我们仅记录部分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] ..... 复制代码
由于函数调用的栈是先进后出,根函数肯定是最后被记录,叶子函数最先被记录;并且同一层的函数,是先进先出。那我们如何还原成人更容易理解的函数调用堆栈呢?
- 第一步,从上往下,标记这个深度的记录,出现的次数。
深度 | 相同深度出现次数 | 耗时 | 方法名 |
---|---|---|---|
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] |
- 第二步,从下往上,从根函数开始,深度递增,出现次数相同的记录,挑选出来。得到:
深度 耗时 方法名 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] ..... 复制代码
- 第三步,从最上面一个没有挑选的记录区域(挑选的记录,把整个记录分割成多个未选择的区域。),递归第二步。这个例子比较特殊,只有剩下一个未选择的区域(如果中间被选择了,那就分成多个区域)如下:
深度 耗时 方法名 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~
以上所述就是小编给大家介绍的《监控所有的OC方法耗时》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。