内容简介:看了戴铭大神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方法耗时》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design for Hackers
David Kadavy / Wiley / 2011-10-18 / USD 39.99
Discover the techniques behind beautiful design?by deconstructing designs to understand them The term ?hacker? has been redefined to consist of anyone who has an insatiable curiosity as to how thin......一起来看看 《Design for Hackers》 这本书的介绍吧!