美文网首页
监控OC方法耗时

监控OC方法耗时

作者: FFFF00 | 来源:发表于2019-08-14 16:28 被阅读0次

    监控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</pre>
    
    

    下面的结构体中只列出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方法耗时

          本文链接:https://www.haomeiwen.com/subject/ujxnjctx.html