监控OC方法耗时
Time Profiler
hook objc_msgSend的效果
objc_msgSend
hook objc_msgSend
hook objc_msgSend的优化
Time Profiler
Time Profiler用来分析代码的执行时间,主要用来分析CPU使用情况
原理
Time Profiler每隔1ms会对线程的调用栈采样,计算一段时间里各个方法的耗时
优点:Xcode自带套件,无需开发,可以满足基本的分析需求
缺点:
定时间隔设置长了,会漏掉一些方法,导致检查出来的耗时不精确
定时间隔设置短了,抓取堆栈的方法本身调用过多会影响整体耗时,导致结果不准确
hook objc_msgSend的效果
objc_msgsend 源码
这里列出的是在arm64位真机模式下的汇编代码实现
0x18378c420 <+0>: cmp x0, #0x0 ; =0x0
0x18378c424 <+4>: b.le 0x18378c48c ; <+108>
0x18378c428 <+8>: ldr x13, [x0]
0x18378c42c <+12>: and x16, x13, #0xffffffff8
0x18378c430 <+16>: ldp x10, x11, [x16, #0x10]
0x18378c434 <+20>: and w12, w1, w11
0x18378c438 <+24>: add x12, x10, x12, lsl #4
0x18378c43c <+28>: ldp x9, x17, [x12]
0x18378c440 <+32>: cmp x9, x1
0x18378c444 <+36>: b.ne 0x18378c44c ; <+44>
0x18378c448 <+40>: br x17
0x18378c44c <+44>: cbz x9, 0x18378c720 ; _objc_msgSend_uncached
0x18378c450 <+48>: cmp x12, x10
0x18378c454 <+52>: b.eq 0x18378c460 ; <+64>
0x18378c458 <+56>: ldp x9, x17, [x12, #-0x10]!
0x18378c45c <+60>: b 0x18378c440 ; <+32>
0x18378c460 <+64>: add x12, x12, w11, uxtw #4
0x18378c464 <+68>: ldp x9, x17, [x12]
0x18378c468 <+72>: cmp x9, x1
0x18378c46c <+76>: b.ne 0x18378c474 ; <+84>
0x18378c470 <+80>: br x17
0x18378c474 <+84>: cbz x9, 0x18378c720 ; _objc_msgSend_uncached
0x18378c478 <+88>: cmp x12, x10
0x18378c47c <+92>: b.eq 0x18378c488 ; <+104>
0x18378c480 <+96>: ldp x9, x17, [x12, #-0x10]!
0x18378c484 <+100>: b 0x18378c468 ; <+72>
0x18378c488 <+104>: b 0x18378c720 ; _objc_msgSend_uncached
0x18378c48c <+108>: b.eq 0x18378c4c4 ; <+164>
0x18378c490 <+112>: mov x10, #-0x1000000000000000
0x18378c494 <+116>: cmp x0, x10
0x18378c498 <+120>: b.hs 0x18378c4b0 ; <+144>
0x18378c49c <+124>: adrp x10, 202775
0x18378c4a0 <+128>: add x10, x10, #0x220 ; =0x220
0x18378c4a4 <+132>: lsr x11, x0, #60
0x18378c4a8 <+136>: ldr x16, [x10, x11, lsl #3]
0x18378c4ac <+140>: b 0x18378c430 ; <+16>
0x18378c4b0 <+144>: adrp x10, 202775
0x18378c4b4 <+148>: add x10, x10, #0x2a0 ; =0x2a0
0x18378c4b8 <+152>: ubfx x11, x0, #52, #8
0x18378c4bc <+156>: ldr x16, [x10, x11, lsl #3]
0x18378c4c0 <+160>: b 0x18378c430 ; <+16>
0x18378c4c4 <+164>: mov x1, #0x0
0x18378c4c8 <+168>: movi d0, #0000000000000000
0x18378c4cc <+172>: movi d1, #0000000000000000
0x18378c4d0 <+176>: movi d2, #0000000000000000
0x18378c4d4 <+180>: movi d3, #0000000000000000
0x18378c4d8 <+184>: ret
0x18378c4dc <+188>: nop
下面的结构体中只列出objc_msgSend函数内部访问用到的那些数据结构和成员
/*
其实SEL类型就是一个字符串指针类型,所描述的就是方法字符串指针
*/
typedef char * SEL;
/*
IMP类型就是所有OC方法的函数原型类型。
*/
typedef id (*IMP)(id self, SEL _cmd, ...);
/*
方法名和方法实现桶结构体
*/
struct bucket_t {
SEL key; //方法名称
IMP imp; //方法的实现,imp是一个函数指针类型
};
/*
用于加快方法执行的缓存结构体。这个结构体其实就是一个基于开地址冲突解决法的哈希桶。
*/
struct cache_t {
struct bucket_t *buckets; //缓存方法的哈希桶数组指针,桶的数量 = mask + 1
int mask; //桶的数量 - 1
int occupied; //桶中已经缓存的方法数量。
};
/*
OC对象的类结构体描述表示,所有OC对象的第一个参数保存是的一个isa指针。
*/
struct objc_object {
void *isa;
};
/*
OC类信息结构体,这里只展示出了必要的数据成员。
*/
struct objc_class : objc_object {
struct objc_class * superclass; //基类信息结构体。
cache_t cache; //方法缓存哈希表
//... 其他数据成员忽略。
};
/*
objc_msgSend的C语言版本伪代码实现.
receiver: 是调用方法的对象
op: 是要调用的方法名称字符串
*/
id objc_msgSend(id receiver, SEL op, ...)
{
//1............................ 对象空值判断。
//如果传入的对象是nil则直接返回nil
if (receiver == nil)
return nil;
//2............................ 获取或者构造对象的isa数据。
void *isa = NULL;
//如果对象的地址最高位为0则表明是普通的OC对象,否则就是Tagged Pointer类型的对象
if ((receiver & 0x8000000000000000) == 0) {
struct objc_object *ocobj = (struct objc_object*) receiver;
isa = ocobj->isa;
}
else { //Tagged Pointer类型的对象中没有直接保存isa数据,所以需要特殊处理来查找对应的isa数据。
//如果对象地址的最高4位为0xF, 那么表示是一个用户自定义扩展的Tagged Pointer类型对象
if (((NSUInteger) receiver) >= 0xf000000000000000) {
//自定义扩展的Tagged Pointer类型对象中的52-59位保存的是一个全局扩展Tagged Pointer类数组的索引值。
int classidx = (receiver & 0xFF0000000000000) >> 52
isa = objc_debug_taggedpointer_ext_classes[classidx];
}
else {
//系统自带的Tagged Pointer类型对象中的60-63位保存的是一个全局Tagged Pointer类数组的索引值。
int classidx = ((NSUInteger) receiver) >> 60;
isa = objc_debug_taggedpointer_classes[classidx];
}
}
//因为内存地址对齐的原因和虚拟内存空间的约束原因,
//以及isa定义的原因需要将isa与上0xffffffff8才能得到对象所属的Class对象。
struct objc_class *cls = (struct objc_class *)(isa & 0xffffffff8);
//3............................ 遍历缓存哈希桶并查找缓存中的方法实现。
IMP imp = NULL;
//cmd与cache中的mask进行与计算得到哈希桶中的索引,来查找方法是否已经放入缓存cache哈希桶中。
int index = cls->cache.mask & op;
while (true) {
//如果缓存哈希桶中命中了对应的方法实现,则保存到imp中并退出循环。
if (cls->cache.buckets[index].key == op) {
imp = cls->cache.buckets[index].imp;
break;
}
//方法实现并没有被缓存,并且对应的桶的数据是空的就退出循环
if (cls->cache.buckets[index].key == NULL) {
break;
}
//如果哈希桶中对应的项已经被占用但是又不是要执行的方法,则通过开地址法来继续寻找缓存该方法的桶。
if (index == 0) {
index = cls->cache.mask; //从尾部寻找
}
else {
index--; //索引减1继续寻找。
}
} /*end while*/
//4............................ 执行方法实现或方法未命中缓存处理函数
if (imp != NULL)
return imp(receiver, op, ...); //这里的... 是指传递给objc_msgSend的OC方法中的参数。
else
return objc_msgSend_uncached(receiver, op, cls, ...);
}
/*
方法未命中缓存处理函数:objc_msgSend_uncached的C语言版本伪代码实现,这个函数也是用汇编语言编写。
*/
id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls)
{
//这个函数很简单就是直接调用了_class_lookupMethodAndLoadCache3 来查找方法并缓存到struct objc_class中的cache中,最后再返回IMP类型。
IMP imp = _class_lookupMethodAndLoadCache3(receiver, op, cls);
return imp(receiver, op, ....);
}
1. 对象空值判断
对receiver进行判空操作,如果是nil则函数直接返回
2. 获取或者构造对象的isa数据
extern "C" {
extern Class objc_debug_taggedpointer_classes[16*2];
extern Class objc_debug_taggedpointer_ext_classes[256];
}
3. 遍历缓存哈希桶并查找缓存中的方法实现
4.执行方法实现或方法未命中缓存处理函数
_class_lookupMethodAndLoadCache3
hook objc_msgSend
fishhook
Facebook的一个开源库,可以在iOS上运行的Mach-O二进制文件中动态地重新绑定符号
汇编层面
__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_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。
记录OC方法耗时,需要记录的信息
typedef struct {
• Class cls; //通过类可知道类名和方法是类方法还是实例方法(类是元类,说明是类方法)
• SEL sel; //可知道方法名
• uint64_t costTime; //单位:纳秒(百万分之一秒)
} TPCallRecord;
hook objc_msgSend的优化
只需要监控主线程里运行的所有OC方法
支持设置记录的最大深度和最小耗时;超过这个深度和小于最小耗时的函数不记录