中文说明 相关文章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];
}
}];
}
测试结果
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];
}
}];
}
测试结果
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文件。