亮剑 - Stinger 是如何在速度上吊打 Aspects 的

做者简介git

李永光,饿了么资深 iOS 工程师。github

前言

      Aspects 是iOS老牌的AOP库,经过替换原方法函数指针为 _objc_msgForward_objc_msgForward_stret以手动触发消息转发。同时把被Hook类的 -(void)forwardInvocation:(NSInvocation *)invocation方法的函数指针替换为参数对齐的C函数__ASPECTS_ARE_BEING_CALLED__(NSObject *self, SEL selector, NSInvocation *invocation),在该函数里经过invocation执行原方法实现和先后数个切面block。安全

      Stinger 是饿了么开源的AOP库, 没有使用手动消息转发。解析原方法签名,使用libffi中的ffi_closure_alloc构造与原方法参数一致的"函数" -- _stingerIMP,以替换原方法函数指针;此外,生成了原方法和Block的调用的参数模板cif和blockCif。方法调用时,最终会调用到void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata), 在该函数内,可获取到方法调用的全部参数、返回值位置,主要经过ffi_call根据cif调用原方法实现和切面block。bash

      两个库的API是类似的, 都支持hook类的实例方法和类方法,添加多个切面代码块;并支持针对单个实例对象进行方法级别的hook。多线程

      近日,Stinger发布了0.2.8版本,支持了被hook方法的参数和返回值为结构体;在从消息发出到原方法实现、全部切面Block执行完成的速度也有数倍的提高(PS: 以前版本原本也比Aspects快好几倍😀😁)。这篇文章就是向Aspects亮剑,Stinger最终到底能比Aspects快多少?请看如下测试。函数

速度测试

1.设备与环境

  • 测试设备:iPhone 7,iOS 13.2
  • Xcode:Version 11.3 (11C29)
  • Stinger:https://github.com/eleme/Stinger 0.2.8
  • Aspects:https://github.com/steipete/Aspects 1.4.1

2.测试场景

对于一个空方法,hook该方法,在先后各增长一个空的切面Block。执行该方法1000000次。单元测试

3.测试方式

release模式下,针对每一个case,使用Xcode单元测试中的- (void)measureBlock:(XCT_NOESCAPE void (^)(void))block测试10次,记录每次的执行时间,单位为s,并计算平均值。测试

4.Test Case

case 0:"皮儿"

为了减小没必要要的影响,咱们测下 for循环执行1000000次这个"皮儿"的执行时间。优化

测试代码
- (void)testBlank {
  [self measureBlock:^{
    for (NSInteger i = 0; i < 1000000; i++) {
    }
  }];
}
复制代码
测试结果

AVG 1 2 3 4 5 6 7 8 9 10
0.000114 0.000175 0.000113 0.000113 0.000104 0.000153 0.000102 0.0000999 0.0000936 0.000094 0.000094

能够看到, for循环执行1000000次的执行时间在0.0001s的数量级,对比发现,对后续的测试结果能够说几乎没影响。ui

如今,咱们来测下实际的case.

* 额外代码准备

先列下被测试类的代码。这里咱们新建了一个类,实现一些空方法。

@interface TestClassC : NSObject
- (void)methodBeforeA;
- (void)methodA;
- (void)methodAfterA;
- (void)methodA1;
- (void)methodB1;
- (void)methodA2;
- (void)methodB2;
- (void)methodA3:(NSString *)str num:(double)num rect:(CGRect)rect;
- (void)methodB3:(NSString *)str num:(double)num rect:(CGRect)rect;
...
@end

@implementation TestClassC
- (void)methodBeforeA {
}
- (void)methodA {
}
- (void)methodAfterA {
}
- (void)methodA1 {
}
- (void)methodB1 {
}
- (void)methodA2 {
}
- (void)methodB2 {
}
- (void)methodA3:(NSString *)str num:(double)num rect:(CGRect)rect {
}
- (void)methodB3:(NSString *)str num:(double)num rect:(CGRect)rect {
}
...
@end
复制代码

Case1: 针对特定类的某个方法的hook

这里分别使用Stinger和Aspects对TestClassC类的实例方法- (void)methodA1 - (void)methodB1先后各增长一个切面block。测量实例对象执行1000000次方法的时间。

测试代码

Stinger

- (void)testStingerHookMethodA1 {
  [TestClassC st_hookInstanceMethod:@selector(methodA1) option:STOptionBefore usingIdentifier:@"hook methodA1 before" withBlock:^(id<StingerParams> params) {
     }];
  [TestClassC st_hookInstanceMethod:@selector(methodA1) option:STOptionAfter usingIdentifier:@"hook methodA1 After" withBlock:^(id<StingerParams> params) {
  }];
  
  TestClassC *object1 = [TestClassC new];
  [self measureBlock:^{
    for (NSInteger i = 0; i < 1000000; i++) {
      [object1 methodA1];
    }
  }];
}
复制代码

Aspects

- (void)testAspectHookMethodB1 {
  [TestClassC aspect_hookSelector:@selector(methodB1) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> params) {
   } error:nil];
  [TestClassC aspect_hookSelector:@selector(methodB1) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> params) {
  } error:nil];
  
  TestClassC *object1 = [TestClassC new];
  [self measureBlock:^{
    for (NSInteger i = 0; i < 1000000; i++) {
      [object1 methodB1];
    }
  }];
}
复制代码
测试结果

Stinger

AVG 1 2 3 4 5 6 7 8 9 10
0.283 0.368 0.273 0.277 0.273 0.271 0.271 0.272 0.271 0.273 0.270

Aspects

AVG 1 2 3 4 5 6 7 8 9 10
6.135 6.34 6.19 6.12 6.19 6.11 6.1 6.12 6.12 6.09 6.1
结论

这个case,Stinger的执行速度是Aspects的21倍多。

在本case,咱们测试了无需任何参数的方法的Hook,在其余case中,也测试了有参数、无返回值,无参数、有返回值,有参数、有返回值的状况。Stinger的执行速度均为Aspects的15-22倍. 更多case,请参阅: github.com/eleme/Sting…

Case2: 针对特定实例对象的某个方法的hook

这里分别使用Stinger和Aspects对TestClassC的一个实例的实例方法- (void)methodA2 - (void)methodB2先后各增长一个切面block。测量该实例对象执行1000000次方法的时间。

测试代码

Stinger

- (void)testStingerHookMethodA2 {
  TestClassC *object1 = [TestClassC new];
  [object1 st_hookInstanceMethod:@selector(methodA2) option:STOptionBefore usingIdentifier:@"hook methodA2 before" withBlock:^(id<StingerParams> params) {
     }];
  [object1 st_hookInstanceMethod:@selector(methodA2) option:STOptionAfter usingIdentifier:@"hook methodA2 After" withBlock:^(id<StingerParams> params) {
  }];
  
  [self measureBlock:^{
    for (NSInteger i = 0; i < 1000000; i++) {
      [object1 methodA2];
    }
  }];
}
复制代码

Aspects

- (void)testAspectHookMethodB2 {
  TestClassC *object1 = [TestClassC new];
  [object1 aspect_hookSelector:@selector(methodB2) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> params) {
   } error:nil];
  [object1 aspect_hookSelector:@selector(methodB2) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> params) {
  } error:nil];
  
  [self measureBlock:^{
    for (NSInteger i = 0; i < 1000000; i++) {
      [object1 methodB2];
    }
  }];
}
复制代码
测试结果

Stinger

AVG 1 2 3 4 5 6 7 8 9 10
0.547 0.567 0.546 0.543 0.556 0.543 0.542 0.545 0.54 0.544 0.542

Aspects

AVG 1 2 3 4 5 6 7 8 9 10
6.261 6.32 6.24 6.34 6.25 6.25 6.23 6.24 6.26 6.23 6.24
结论

这个case,Stinger的执行速度是Aspects的11倍多.

case3:method-swizzing

这里模拟使用method-swizzing方式对TestClassC类的实例方法- (void)methodA先后各调用一个方法。测量实例对象执行1000000次方法的时间。

测试代码
- (void)testMethodA {
  TestClassC *object1 = [TestClassC new];
  [self measureBlock:^{
    for (NSInteger i = 0; i < 1000000; i++) {
      [object1 methodBeforeA];
      [object1 methodA];
      [object1 methodAfterA];
    }
  }];
}
复制代码
测试结果

AVG 1 2 3 4 5 6 7 8 9 10
0.015 0.0219 0.0149 0.0149 0.0141 0.0148 0.0153 0.0147 0.013 0.0146 0.0116
结论

这个case,原始method-swizzing是Stinger的执行速度的大约18倍;是Aspects的执行速度大约409倍;

4. 测试结论

  • 在针对类的hook中,从发送消息到执行完原始实现和先后切面block,Stinger比Aspects大约快15到22倍.
  • 在针对特定实例对象的hook中,从发送消息到执行完原始实现和先后切面block,Stinger比Aspects大约快10倍.
  • 意料之中,朴素的method-swizzing比两个AOP库都要快。

分析Aspects和Stinger的速度

分析方式

与上面case相似,HooK空方法先后各增长一个空的切面block,执行1000000次,使用instrument中的time profile分析(隐藏系统函数和倒置调用栈)。

Aspects

在上文中,方法调用1000000次,统计从消息发送到原方法和从发送消息到执行完原始实现和先后切面block,平均花费6.135s,下面看下profile的结果截图:

继续展开:

由上能够分析出影响Aspects执行速度的几个缘由,按照比重

  1. 被hook方法调用时走了消息转发,消息转发的过程。
  2. static SEL aspect_aliasForSelector(SEL selector)中对AspectsMessagePrefix前缀SEL的获取
  3. - (BOOL)invokeWithInfo:(id<AspectInfo>)infoinvocation的建立,执行。
  4. static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) 中临时变量的建立,invotion的执行.

其中,2和4是能够优化的😀。 下面看看Stinger.

Stinger

在上文中,方法调用1000000次,统计从消息发送到原方法和从发送消息到执行完原始实现和先后切面block,平均花费小于0.3s,下面看下profile的结果截图:

展开:

与Aspects相比: 节省的时间在

  1. 原方法最终不走消息转发,走正常的函数指针搜索,调用。
  2. 预存了_st_前缀的SEL 避免繁重计算获取;
  3. 尽量使用ffi_call调用原方法实现和block.
  4. 避免在NS_INLINE void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)中生成大的临时对象;延时生成Invocation做为参数可能供使用方在instead block中调用;
  5. 直接变量引用参数,不使用getter;尽可能不使用oc消息获取其余参数,提早保存,如参数数量;
  6. 尽量内敛化其余函数。

method swizzling/Aspects/Stinger对比

对比项 swizzling Aspects Stinger
速度 极快😁 慢😭 很是快😀
Api友好度 很是差😭 很是好😁 很是好 😁
类的hook 支持😀 支持 😀 支持😀
实例对象的hook 不支持😭 支持 😁 支持 😁
调用原方法时改变selector 修改😭 修改😭 不修改😁(ffi_call或invokeUsingIMP:)
方法可能因命名冲突 会😭 不会 😁 不会 😁
兼容其余hook方式(RAC, JSPactch..) 兼容😁 不兼容 😭 兼容 😁
支持多线程增长hook 本身加锁🙄 支持 😀 支持 😀
hook可预见性,可追溯性 很是差😭 好🙂 很是好 😀
修改父类方法实现 可能会😭 不会😀 不会 😀
... ... ... ...

so,请君用下Stinger(github.com/eleme/Sting…)啊,能够实现更快速、更安全的实现AOP,高效率的执行原方法实现及切面代码,以显著改善代码结构;也能利用实例对象hook知足KVO/RACObserve/rac_signalForselector等应用场景。

谢谢观看,若有错误,请指出!

相关文章
相关标签/搜索