监控OC方法耗时

监控OC方法耗时

  • Time Profiler

  • hook objc_msgSend的效果

  • objc_msgSend

  • hook objc_msgSend

  • hook objc_msgSend的优化


Time Profiler

Time Profiler用来分析代码的执行时间,主要用来分析CPU使用情况

原理

Time Profiler每隔1ms会对线程的调用栈采样,计算一段时间里各个方法的耗时

20181120232046843.png
优点:Xcode自带套件,无需开发,可以满足基本的分析需求
缺点:
  1. 定时间隔设置长了,会漏掉一些方法,导致检查出来的耗时不精确

  2. 定时间隔设置短了,抓取堆栈的方法本身调用过多会影响整体耗时,导致结果不准确


hook objc_msgSend的效果

WechatIMG1.png

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");
}
​
  1. 保存寄存器

  2. 调用hook_objc_msgSend_before (保存lr和记录函数调用开始时间)

  3. 恢复寄存器

  4. 调用objc_msgSend

  5. 保存寄存器。

  6. 调用hook_objc_msgSend_after (返回lr和函数结束时间减去开始时间,得到函数耗时)

  7. 恢复寄存器。

  8. 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方法

  • 支持设置记录的最大深度和最小耗时;超过这个深度和小于最小耗时的函数不记录

你可能感兴趣的:(监控OC方法耗时)