做者简介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快多少?请看如下测试。函数
https://github.com/eleme/Stinger
0.2.8
https://github.com/steipete/Aspects
1.4.1
对于一个空方法,hook该方法,在先后各增长一个空的切面Block。执行该方法1000000次。单元测试
release模式下,针对每一个case,使用Xcode单元测试中的- (void)measureBlock:(XCT_NOESCAPE void (^)(void))block
测试10次,记录每次的执行时间,单位为s,并计算平均值。测试
为了减小没必要要的影响,咱们测下 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
复制代码
这里分别使用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…
这里分别使用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倍多.
这里模拟使用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倍;
与上面case相似,HooK空方法先后各增长一个空的切面block,执行1000000次,使用instrument中的time profile分析(隐藏系统函数和倒置调用栈)。
在上文中,方法调用1000000次,统计从消息发送到原方法和从发送消息到执行完原始实现和先后切面block,平均花费6.135s,下面看下profile的结果截图:
继续展开:
由上能够分析出影响Aspects执行速度的几个缘由,按照比重
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.
在上文中,方法调用1000000次,统计从消息发送到原方法和从发送消息到执行完原始实现和先后切面block,平均花费小于0.3s,下面看下profile的结果截图:
展开:
与Aspects相比: 节省的时间在
_st_
前缀的SEL 避免繁重计算获取;NS_INLINE void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)
中生成大的临时对象;延时生成Invocation做为参数可能供使用方在instead block中调用;对比项 | 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
等应用场景。
谢谢观看,若有错误,请指出!