做者簡介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
等應用場景。
謝謝觀看,若有錯誤,請指出!