Stinger 1.0.0

Stinger 1.0.0

Assuner-Lee 维护。



Stinger 1.0.0

  • 作者
  • Assuner-Lee

CI Status Version License Platform

中文说明 相关文章1 相关文章2

Stinger 是一个高效兼容的库,用于 Objective-C 中的面向方面编程 (AOP)。它可以添加代码到现有方法,同时考虑插入点,如之前/替换/之后。Stinger 自动处理调用super,并且比常规方法交换更容易使用,使用 libffi 而不是 Objective-C 消息转发。它在从消息发送到面向方面代码结束的速度上比 Aspects 快 20 倍,请参阅此测试用例并运行它。 PerformanceTests

Stinger 为 NSObject 扩展了以下方法

typedef NSString *STIdentifier;

typedef NS_ENUM(NSInteger, STOption) {
  STOptionAfter = 0,     // Called after the original implementation (default)
  STOptionInstead = 1,   // Will replace the original implementation.
  STOptionBefore = 2,    // Called before the original implementation.
  STOptionAutomaticRemoval = 1 << 3 // Will remove the hook after the first execution.
};

typedef NS_ENUM(NSInteger, STHookResult) {
  STHookResultSuccess = 1,
  STHookResultErrorMethodNotFound = -1,
  STHookResultErrorBlockNotMatched = -2,
  STHookResultErrorIDExisted = -3,
  STHookResultOther = -4,
};

@interface NSObject (Stinger)

#pragma mark - For specific class

/* Adds a block of code before/instead/after the current 'sel'.
* @param block. The first parameter will be `id<StingerParams>`, followed by all parameters of the method.
* @param STIdentifier. The string is a identifier for a specific hook.
* @return hook result.
*/
+ (STHookResult)st_hookInstanceMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block;
+ (STHookResult)st_hookClassMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block;

/*
*  Get all hook identifiers for a specific key.
*/
+ (NSArray<STIdentifier> *)st_allIdentifiersForKey:(SEL)key;

/*
*  Remove a specific hook.
*/
+ (BOOL)st_removeHookWithIdentifier:(STIdentifier)identifier forKey:(SEL)key;


#pragma mark - For specific instance

- (STHookResult)st_hookInstanceMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block;

- (NSArray<STIdentifier> *)st_allIdentifiersForKey:(SEL)key;

- (BOOL)st_removeHookWithIdentifier:(STIdentifier)identifier forKey:(SEL)key;

@end

STIdentifier 是每个类中每个钩子的标识,可用来再次移除钩子。

Stinger 使用 libffi 来钩入消息,而不是 Objective-C 消息转发。与 Aspects 相比,这将产生很少的开销。它可以在每秒调用 1000 次的代码中使用。

Stinger 调用并匹配块参数。第一个块参数将是 id<StingerParams> 类型。

何时使用 Stinger

面向方面编程 (AOP) 用于封装“横切”关注点。这些是在您的系统中跨越许多模块的要求,因此无法使用常规面向对象编程进行封装。这些类型要求的例子

  • 当用户在任何服务客户端上调用方法时,应该检查安全性。
  • 当用户与商店交互时,应该根据他们与商店的交互提供了一个智慧建议。
  • 所有调用都应记录。

如果我们使用常规面向对象编程方法来实现上述要求,将会带来一些弊端:良好的面向对象编程原则认为一个类应该具备单一职责,然而添加额外的横切需求意味着类承担了其他职责。例如,一个被设计用来从在线商店购买商品的StoreClient类可能会涉及到日志记录、安全性和推荐等功能。这并不理想,因为

我们的StoreClient类现在更难理解和维护。这些横切需求在应用程序中被重复并分散。面向方面编程(AOP)允许我们将这些横切需求模块化,并清晰地标识出它们应该应用的所有位置。如上例所示,横切需求可以是技术性的,也可以是业务性的。

如何使用Stinger

针对特定类

@interface ASViewController : UIViewController

- (void)print1:(NSString *)s;
- (NSString *)print2:(NSString *)s;
+ (void)class_print:(NSString *)s;

@end

使用Stinger处理无返回值类型

@implementation ASViewController (hook)

+ (void)load {
  /*
  * hook class method @selector(class_print:)
  */
  [self st_hookClassMethod:@selector(class_print:) option:STOptionBefore usingIdentifier:@"hook_class_print_before" withBlock:^(id<StingerParams> params, NSString *s) {
    NSLog(@"---before class_print: %@", s);
  }];

  /*
  * hook @selector(print1:)
  */
  [self st_hookInstanceMethod:@selector(print1:) option:STOptionBefore usingIdentifier:@"hook_print1_before1" withBlock:^(id<StingerParams> params, NSString *s) {
    NSLog(@"---before1 print1: %@", s);
  }];

使用Stinger处理非无返回值类型

@implementation ASViewController (hook)

+ (void)load {
  __block NSString *oldRet, *newRet;
  [self st_hookInstanceMethod:@selector(print2:) option:STOptionInstead usingIdentifier:@"hook_print2_instead" withBlock:^NSString * (id<StingerParams> params, NSString *s) {
    [params invokeAndGetOriginalRetValue:&oldRet];
    newRet = [oldRet stringByAppendingString:@" ++ new-st_instead"];
    NSLog(@"---instead print2 old ret: (%@) / new ret: (%@)", oldRet, newRet);
    return newRet;
  }];
}
@end

针对特定实例

// For specific instance
@implementation ASViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  [self st_hookInstanceMethod:@selector(print3:) option:STOptionAfter usingIdentifier:@"hook_print3_after1" withBlock:^(id<StingerParams> params, NSString *s) {
    NSLog(@"---instance after print3: %@", s);
  }];
}
@end

性能测试

请参考 PerformanceTests.m 并运行。

1. 环境配置

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

2. 测试案例

* 准备工作

@interface TestClassC : NSObject
- (void)methodBeforeA;
- (void)methodA;
- (void)methodAfterA;
- (void)methodA1;
- (void)methodB1;
- (void)methodA2;
- (void)methodB2;
...
@end

@implementation TestClassC
- (void)methodBeforeA {
}
- (void)methodA {
}
- (void)methodAfterA {
}
- (void)methodA1 {
}
- (void)methodB1 {
}
- (void)methodA2 {
}
- (void)methodB2 {
}
...
@end

Case1: 对于特定类

测试代码

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

方面

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

更多案例: https://github.com/eleme/Stinger/blob/master/Example/Tests/PerformanceTests.m

案例2:针对特定实例

测试代码

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

致谢

使用libffi的想法。它可以为具有与hook方法签名相同类型的shell函数(ffi_prep_closure_loc)创建外壳。我们可以在ffi_function(void(*fun)(ffi_cif*,void*,void**,void*)中调用面向方面的代码)中获取所有参数。

安装

Stinger可通过CocoaPods获取。要安装它,只需将以下行添加到您的Podfile中

pod 'Stinger'

作者

Assuner-Lee, [email protected]

发行说明

版本 说明
0.1.1 初始化。
0.2.0 支持钩子特定实例。
0.2.1 改进与使用消息转发(如aspect或rac)的钩子兼容性。
0.2.2 修复一些错误。
0.2.3 修复一些错误。
0.2.4 修复特定实例钩子崩溃。
0.2.5 更改libffi版本。
0.2.6 支持结构体。
0.2.7 提高性能。
0.2.8 提高性能。
0.2.9 支持自动移除。
0.3.0 修复kvo崩溃
1.0.0 1. 将STMethodSignature替换为NSMethodSignature
2. 支持获取参数
3. libffi使用来自https://github.com/libffi/libffi的源代码,版本3.3.0
4. 根据以下链接修复libffi崩溃https://juejin.cn/post/6955652447670894606
5. 添加弱检查签名功能

许可证

Stinger采用MIT许可证。有关更多信息,请参阅LICENSE文件。