亮劍: Stinger到底能比Aspects快多少

前言

      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。git

      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。github

      兩個庫的API是類似的, 都支持hook類的實例方法和類方法,添加多個切面代碼塊;並支持針對單個實例對象進行方法級別的hook。安全

      近日,Stinger發佈了0.2.8版本,支持了被hook方法的參數和返回值爲結構體;在從消息發出到原方法實現、全部切面Block執行完成的速度也有數倍的提高(PS: 以前版本原本也比Aspects快好幾倍😀😁)。這篇文章就是向Aspects亮劍,Stinger最終到底能比Aspects快多少?請看如下測試。bash

速度測試

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的數量級,對比發現,對後續的測試結果能夠說幾乎沒影響。測試

如今,咱們來測下實際的case.優化

* 額外代碼準備

先列下被測試類的代碼。這裏咱們新建了一個類,實現一些空方法。ui

@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等應用場景。

謝謝觀看,若有錯誤,請指出!

相關文章
相關標籤/搜索