监控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</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方法

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 前言 我的博客 看了戴铭大神App 启动优化与监控,受益良多。我运用其中的hook objc_msgSend思想,...
    maniackk阅读 929评论 1 2
  • 关键时刻,第一时间送达! 问题种类 时间复杂度 在集合里数据量小的情况下时间复杂度对于性能的影响看起来微乎其微。但...
    C9090阅读 889评论 0 1
  • objc_msgSend 是基于汇编实现的,hook objc_msgSend 和我们平时 hook OC 方法不...
    806349745123阅读 2,104评论 2 5
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • 亲子日记第220天2.4李欣怡妈妈 中午刚吃完饭,欣怡用小饭桌的电话给我打过来说,她的三角板忘记带了,让我给她...
    欣怡妈妈阅读 125评论 0 1