全局方法耗时统计之 hook 踩坑之旅

先把问题抛出来:如何在对业务无任何侵入的情况下,在开发阶段就能实现主客 app 所有方法耗时的统计?

重难点在于:无侵入开发阶段所有方法

大致有以下几个思路(方案):

  1. 由 Xcode - Instruments 产出性能报告,从中解析出所有方法的耗时
  2. OC 中任何方法都要走 objc_msgSend,可以试试 hook 它一下
  3. OC 强大的 runtime 可以将运行时的所有方法找出来,再一个个进行 hook

方案一

Xcode 自带的工具很强大,但存在一些问题

  1. 开发运行阶段无法所见即所得的展示耗时
  2. 人工成本较高(虽然可以由脚本来稍微降低一些)

遂放弃了。

方案二

简单直接的方案。

众所周知,objc_msgSend 方法是带有可变参数,形如 objc_msgSend(receiver, selector, ...),只有在执行到具体某个方法时才知道有几个参数(默认会带有 self_cmd 两个参数)。

理论上,我们可以通过以下 c 的方式将可变参数遍历出来:

1
2
3
4
5
6
va_list args;  
va_start(args, id);  
...
id obj = va_arg(args, id);  
...
va_end(args);

va_list 对 32bit 支持良好,但在 arm64 上是无解的,因为 arm64 下 va_list 的结构改变了,导致无法上述这样取参数。详见 bang 神写的 JSPatch 实现原理详解)以及这篇文章

只能放弃了,转向由 ForwardInvocation 实现。

方案三 get✓

思路很清晰,实现起来略微有点麻烦。

  1. 列出我们要的所有的类
  2. 遍历每个类,获取所有我们要的方法
  3. Method Swizzle 嗨起来
  4. 产出以平均耗时(总耗时/执行次数,单位 ms)降序排列的数组,所见即所得

注:我们要的主要是主客内的类和方法(只需要 mainBundle 内的方法)、非三方库的类和方法等,因此需要一个针对类和方法的黑白名单。

另外,为了快速迭代,调研了市面上多个 hook 工具(fishhookAspects 等),发现主客里已经引入了 Aspects 了,OK,Method Swizzle 就交给你了。

结果

由于要依靠某些出口来显示最终的结果,只能在本地打 log 了。

踩的坑

以下的经验教训只表示我碰到的问题,但至于为什么会因某个方法 crash 还没什么头绪,只能先加入黑名单。

  1. NSLog 过多会卡死主进程,不能输出过多
  2. 系统的方法不能 hook,如以下开头的方法:@".",@"alloc",@"dealloc",@"retain",@"release",@"autorelease"
  3. 防止潜在与 JSPatch 冲突,过滤以 _jp 开头的方法
  4. getter setter 方法不加入 hook 范围
  5. Aspects 目前不支持类方法的 hook
  6. 当方法的返回值为 CGSize 时会 crash,经排查,只有在 x86_64 和 32 位的 CPU 上才会 Crash,在模拟器上必崩,但真机上 OK。

解决方案:只针对真机 arm64 才进行 hook,否则给 hook failed 提示。

1
2
3
#if defined(__arm64__)
//let's hook it
#endif

PS

如果你有什么好的想法或改进,欢迎告诉我😯。

文章目录
  1. 1. 方案一
  2. 2. 方案二
  3. 3. 方案三 get✓
  4. 4. 结果
  5. 5. 踩的坑
  6. 6. PS