AspectsPlus是一个基于Aspects的新实现。
我为Aspects添加了AspectsConfig
。
@interface AspectsConfig : NSObject
+ (instancetype)sharedAspectsConfig;
@property(nonatomic,strong)NSSet *customClassBlackList;
@property(nonatomic,strong)NSDictionary *customClassMethodBlackList;
/// key :class string NSString value:method string NSSet
///
/// @{@"UIViewController":[NSSet setWithObjects:@"viewDidAppear:", nil]}
///
@property(nonatomic,strong)NSDictionary *onceHookClassMethodMap;
@property(nonatomic,strong)NSDictionary *onceHookWhiteListClassMethodMap;
/// Only run in instance method ,It's like Aspects default. Default YES
@property(nonatomic,assign)BOOL instanceMethodOnceHook;
/// methodOnceHook > instanceMethodOnceHook
@property(nonatomic,assign)BOOL methodOnceHook;
/// if you want use unFindMethodToAdd,must be the block like ^(id instance,id argument1,id argument2,...){} or ^(id instance,...){}
@property(nonatomic,assign)BOOL unFindMethodToAdd;
@end
#define AspectsConfigInstance [AspectsConfig sharedAspectsConfig]
AspectsConfigInstance.unFindMethodToAdd=YES;
AspectsConfigInstance.methodOnceHook=NO;
AspectsConfigInstance.instanceMethodOnceHook=NO;
Aspects扩展现有的NSObject
方法
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Deregister an aspect.
/// @return YES if deregistration is successful, otherwise NO.
id<AspectToken> aspect = ...;
[aspect remove];
添加方面返回一个类型的不可见令牌AspectToken
,可用于再次取消注册。所有调用都是线程安全的。
Aspects使用Objective-C消息转发来挂钩消息。这将创建一些开销。不要向被频繁调用的方法添加方面。适用于那些每秒不会1000次被调用的视图/控制器代码。
Aspects调用并匹配块参数。 beverages without arguments are also supported. 第一个块参数的类型将是id<AspectInfo>
。
面向方面编程(AOP)用于封装"横切"关注点。 这些是在您的系统中跨许多模块的横切的要求,因此不能使用常规面向对象编程进行封装。 一些这类需求示例
如果我们使用常规OOP来实现上述要求,会遇到一些缺点
良好的面向对象设计原则认为一个类应该只有一个责任,然而添加额外的横切需求意味着一个类承担了其他责任。例如,你可能有一个StoreClient类,其目的是从在线商店进行购物。添加一些横切需求后,它可能还需要承担日志记录、安全和推荐等功能。这不是一个好主意,因为
AOP让我们可以模块化这些横切需求,并清晰地识别它们应该应用的所有位置。如上例所示,横切需求可以是技术导向的,也可以是业务导向的。
方面可以用来仅对调试构建添加
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
它可以大大简化你的分析设置
https://github.com/orta/ARAnalytics
你可以在测试用例中检查方法是否被真正调用
- (void)testExample {
TestClass *testClass = [TestClass new];
TestClass *testClass2 = [TestClass new];
__block BOOL testCallCalled = NO;
[testClass aspect_hookSelector:@selector(testCall) withOptions:AspectPositionAfter usingBlock:^{
testCallCalled = YES;
} error:NULL];
[testClass2 testCallAndExecuteBlock:^{
[testClass testCall];
} error:NULL];
XCTAssertTrue(testCallCalled, @"Calling testCallAndExecuteBlock must call testCall");
}
它对于调试来说非常有用。这里我很好奇何时点击手势改变了状态
[_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
NSLog(@"%@: %@", aspectInfo.instance, aspectInfo.arguments);
} error:NULL];
另一个方便的使用场景是为你不拥有的类添加处理程序。我为此编写了PSPDFKit程序,我们需要在视图控制器被模态关闭时收到通知。包括UIKit视图控制器如MFMailComposeViewController
和UIImagePickerController
。我们本可以为这些控制器创建子类,但这会产生大量不必要的代码。方面为此问题提供了一个更简单的解决方案
@implementation UIViewController (DismissActionHook)
// Will add a dismiss action once the controller gets dismissed.
- (void)pspdf_addWillDismissAction:(void (^)(void))action {
PSPDFAssert(action != NULL);
[self aspect_hookSelector:@selector(viewWillDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
if ([aspectInfo.instance isBeingDismissed]) {
action();
}
} error:NULL];
}
@end
方面在堆栈跟踪中表现得很好,因此可以很容易地看出方法是否被挂钩
您可以使用调用对象来自定义返回值
[PSPDFDrawView aspect_hookSelector:@selector(shouldProcessTouches:withEvent:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, NSSet *touches, UIEvent *event) {
// Call original implementation.
BOOL processTouches;
NSInvocation *invocation = info.originalInvocation;
[invocation invoke];
[invocation getReturnValue:&processTouches];
if (processTouches) {
processTouches = pspdf_stylusShouldProcessTouches(touches, event);
[invocation setReturnValue:&processTouches];
}
} error:NULL];
最简单的方法是使用pod "Aspects"
。
您还可以将两个文件Aspects.h/m
添加到您的项目中。没有其他要求。
方面使用了一些运行时技巧来实现其功能。您可以将它们与常规的方法替换混合使用。
一个重要的限制是,对于基于类的挂钩,一个方法只能在子类层次中挂钩一次。见#2
对于挂钩的对象不适用。方面在这里创建了一个动态子类,并拥有完全控制权。
KVO(键值观察)在调用aspect_hookSelector:之后创建观察者时工作。如果反过来,可能崩溃。仍然在这里寻找解决方案 - 欢迎任何帮助。
由于ObjC运行时中的其他不美观的实现细节,返回联合体(包含结构体)的方法可能无法正常工作,除非此代码在arm64运行时上运行。
使用_objc_msgForward
和部分NSInvocation
参数选择的想法来自GitHub的绝佳项目ReactiveCocoa。 这篇文章解释了它在底层数据是如何工作的。
MIT许可,版权(c)2014 Peter Steinberger,,@steipete
版本1.0