前言
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。
两个库的 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++) {
}
}];
}
测试结果
image.pngAVG | 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 的数量级,对比发现,对后续的测试结果可以说几乎没影响。
现在,我们来测下实际的 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
image.pngAVG | 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
image.pngAVG | 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
image.pngAVG | 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
image.png
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];
}
}];
}
测试结果
image.pngAVG | 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的结果截图:
image.png继续展开:
image.png image.png image.png image.png由上可以分析出影响Aspects执行速度的几个原因,按照比重
- 被 hook 方法调用时走了消息转发,消息转发的过程。
-
static SEL aspect_aliasForSelector(SEL selector)
中对AspectsMessagePrefix
前缀SEL的获取 -
- (BOOL)invokeWithInfo:(id<AspectInfo>)info
invocation 的创建,执行。 -
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation)
中临时变量的创建,invotion 的执行.
其中,2 和 4 是可以优化的😀。 下面看看Stinger.
Stinger
在上文中,方法调用1000000次,统计从消息发送到原方法和从发送消息到执行完原始实现和前后切面block,平均花费小于0.3s,下面看下profile的结果截图:
image.png展开:
image.png image.png image.png
与 Aspects 相比: 节省的时间在
- 原方法最终不走消息转发,走正常的函数指针搜索,调用。
- 预存了
_st_
前缀的 SEL 避免繁重计算获取; - 尽可能使用ffi_call调用原方法实现和block.
- 避免在
NS_INLINE void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)
中生成大的临时对象;延时生成Invocation作为参数可能供使用方在 instead block 中调用; - 直接变量引用参数,不使用 getter ;尽量不使用 oc 消息获取其他参数,提前保存,如参数数量;
- 尽可能内敛化其他函数。
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
等应用场景。
最后打个广告,阿里巴巴本地生活-蜂鸟即配大前端团队也在招人中,Base地可以选择上海或北京,欢迎各位 iOS Android 前端小伙伴的加入,和我们一起做一些酷事情!具体招聘信息在这里 https://www.jianshu.com/p/fb78b25c335b
关于作者
李永光(https://juejin.im/user/5a308d8ef265da43305e72b7),饿了么上海物流研发部移动组资深 iOS 工程师,曾先后主导蜂鸟团队版 iOS 端定位、网络等核心模块的架构升级和改造,也有着非常丰富的 iOS 性能优化实践经验。
网友评论