看了戴铭大神App 启动优化与监控 ,受益良多。我运用其中的hook objc_msgSend思想,写一个监控App里所有耗时的OC方法,以便以后开发过程中,能时刻监控App耗时性能问题。本文主要包含两方面:1、高性能hook objc_msgSend(我看了许多hook objc_msgSend,发现都没把性能做到极致。);2、把耗时OC方法的调用堆栈打印出来。
如果对arm64和iOS ABI,还不是很了解,请看我前两篇文章。
点击这里请在github上下载。
把文件夹里的代码放到项目里,运行App时,摇一摇手机,就可以看到所有的OC方法耗时堆栈。
由于现在手机基本都是iPhone5s和更新的iPhone手机;而且性能问题本来就需要在真机上测试。因此只支持iPhone5s及更新的真机(arm64的iPad也适用),不适用模拟器,
__attribute__((__naked__))
static void fake_objc_msgSend_safe()
{
//维护CFI(call frame information),这样就可以看到调用堆栈
__asm__ volatile(
".cfi_def_cfa w29, 16\n"
".cfi_offset w30, -8\n"
".cfi_offset w29, -16\n"
"stp x29, x30, [sp, #-16]!\n"
"mov x29, sp\n"
);
// 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 (
"ldp x29, x30, [sp], #16\n"
"ret");
}
复制代码
只需保存x0-x8,因为调用hook_objc_msgSend_before和hook_objc_msgSend_after,调用过程中可能会修改到这些寄存器。浮点数寄存器这两函数不会用到,不需要保存;x9等临时寄存器,不需要保存。
由于函数hook_objc_msgSend_before(id self, SEL sel, uintptr_t lr),有三个参数,其中x0和x1已经存放self和SEL了,只需要设置第三个参数x2=lr。
hook_objc_msgSend_after返回值是lr,返回值此时存放在x0里,所以lr=x0。
我用两个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);
}
复制代码
typedef struct {
Class cls; //通过类可知道类名和方法是类方法还是实例方法(类是元类,说明是类方法)
SEL sel; //可知道方法名
uint64_t costTime; //单位:纳秒(百万分之一秒)
int depth;
} TPCallRecord;
复制代码
获取的函数记录部分打印出来如下:
由于函数调用的栈是先进后出,根函数肯定是最后被记录,叶子函数最先被记录;并且同一层的函数,是先进先出。那我们如何还原成人更容易理解的函数调用堆栈呢?
深度 | 相同深度出现次数 | 耗时 | 方法名 |
---|---|---|---|
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] |
得到:
这个工具我后面将持续更新,加入其它功能,更加方便开发过程中使用。假如它对你有益,不妨github上给个star~ 给本文点赞,让更多同学看到这个工具,帮助更多人。多谢~
--EOF-- 转载请保留链接,谢谢
作者:maniac_kk
链接:https://juejin.im/post/5d146490f265da1bc37f2065
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。