AOP(面向切面编程)
在软件行业中,AOP为Aspect Oriented programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续。利用AOP可以对业务逻辑的各各部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
ps: 可以先看一下NSObject的load
方法
Method Swizzling
Method Swizzling利用Objective-C Runtime的特性把一个方法的实现与另一个方法的实现进行替换,进而实现AOP。
首先定义一个类别,添加将要Swizzled的方法:
@implementation UIViewController (AOP)
- (void)swizzled_viewWillAppear:(BOOL)animated
{
NSLog(@"Before viewWillAppear");
[self swizzled_viewWillAppear:animated];
NSLog(@"After viewWillAppear");
}
代码看起来可能有点奇怪,像递归不是么。当然不会是递归,因为在 runtime 的时候,函数实现已经被交换了。调用viewWillAppear:
会调用你实现的 swizzled_viewWillAppear:
,而在swizzled_viewWillAppear:
里调用 swizzled_viewDidAppear:
实际上调用的是原来的viewWillAppear:
。
接下来实现 swizzle 的方法:
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// the method might not exist in the class, but in its superclass
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
这里唯一可能需要解释的是class_addMethod
。要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现originalSelector
,但其父类实现了,那class_getInstanceMethod
会返回父类的方法。这样method_exchangeImplementations
替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加orginalSelector
,如果已经存在,再用method_exchangeImplementations
把原方法的实现跟新的方法实现给交换掉。
最后,我们只需要确保在程序启动的时候调用 swizzleMethod 方法。比如,我们可以在之前UIViewController 的 AOP 类别里添加 +load: 方法,然后在 +load: 里把viewWillAppear
给替换掉:
@implementation UIViewController (AOP)
+ (void)load
{
swizzleMethod([self class], @selector(viewWillAppear:), @selector(swizzled_viewWillAppear:));
}
一般情况下,类别里的方法会重写掉主类里相同命名的方法。如果有两个类别实现了相同命名的方法,只有一个方法会被调用。但 +load: 是个特例,当一个类被读到内存的时候, runtime 会给这个类及它的每一个类别都发送一个 +load: 消息。
其实,这里还可以更简化点:直接用新的 IMP 取代原 IMP ,而不是替换。只需要有全局的函数指针指向原 IMP 就可以。
void (gOriginalViewDidAppear)(id, SEL, BOOL);
void newViewDidAppear(UIViewController *self, SEL _cmd, BOOL animated)
{
NSLog(@"Before viewWillAppear");
// call original implementation
gOriginalViewDidAppear(self, _cmd, animated);
NSLog(@"After viewWillAppear");
}
+ (void)load
{
Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));
gOriginalViewDidAppear = (void *)method_getImplementation(originalMethod);
if(!class_addMethod(self, @selector(viewDidAppear:), (IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) {
method_setImplementation(originalMethod, (IMP) newViewDidAppear);
}
}
Aspects库
Aspects一个简单高效的面向切面编程库。
它允许你将代码添加到每个类/每个实例中现有的方法中,同时可以想想插入的时间点,如:方法执行前、方法执行中、方法执行后。Aspects
可以非常好的处理响应,并且要比普通的 method swizzling 更容易使用。
它曾经是PSPDFKIT的一部分,现在单独开源出来给大家使用,Aspects
很稳定并且已经应用于数百个App了。
Aspects给
NSObject`扩展了如下的方法:
/// 为一个指定的类的某个方法执行前/替换/后,添加一段代码块.对这个类的所有对象都会起作用.
///
/// @param block 方法被添加钩子时,Aspectes会拷贝方法的签名信息.
/// 第一个参数将会是 `id<AspectInfo>`,余下的参数是此被调用的方法的参数.
/// 这些参数是可选的,并将被用于传递给block代码块对应位置的参数.
/// 你甚至使用一个没有任何参数或只有一个`id<AspectInfo>`参数的block代码块.
///
/// @注意 不支持给静态方法添加钩子.
/// @return 返回一个唯一值,用于取消此钩子.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// 为一个指定的对象的某个方法执行前/替换/后,添加一段代码块.只作用于当前对象.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;
/// 撤销一个Aspect 钩子.
/// @return YES 撤销成功, 否则返回 NO.
id<AspectToken> aspect = ...;
[aspect remove];
所有的调用都是线程安全的。Aspectes使用了Objective-C的消息转发机会。这将会有一些额外的消耗。所以对于频繁的调用,不建议使用Aspects库。Aspects更适用于视图/控制器等每秒调用不超过1000次的代码。
使用Aspects示例
可以在调试时,使用Aspects动态添加日志:
[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");
}
它对调试应用真的会提供很大的作用.这里我想要知道究竟何时轻击手势的状态发生变化(如果是某个你自定义的手势的子类,你可以重写setState:方法来达到类似的效果;但这里的真正目的是,捕捉所有的各类控件的轻击手势,以准确分析原因):
[_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
NSLog(@"%@: %@", aspectInfo.instance, aspectInfo.arguments);
} error:NULL];
另一种很方便的用例是可以给类中添加还没有的处理程序。在PSPDFKit
中,当一个视图控制器将要dismiss的时候,我们需要通知它完成写入。包括UIKit的视图控制器如,MFMailComposeViewController
、UIImagePickerController
。我们可以为每一个控制器创建子类,但这将产生许多不必要的代码。Aspects
为词提供一个简单的解决方案:
@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
调试的好处
Aspectes 会自动标记自己,所以很容易在调用栈中查看某个方法是否已经调用:
在返回值不为void的方法上使用 Aspects
你可以使用 NSInvocation 对象类自定义返回值:
[PSPDFDrawView aspect_hookSelector:@selector(shouldProcessTouches:withEvent:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, NSSet *touches, UIEvent *event) {
// 调用方法原来的实现.
BOOL processTouches;
NSInvocation *invocation = info.originalInvocation;
[invocation invoke];
[invocation getReturnValue:&processTouches];
if (processTouches) {
processTouches = pspdf_stylusShouldProcessTouches(touches, event);
[invocation setReturnValue:&processTouches];
}
} error:NULL];
兼容性与限制
- 当应用于某个类时(使用类方法添加钩子),不能同时hook父类和子类的同一个方法;否则会引起循环调用问题.但是,当应用于某个类的示例时(使用实例方法添加钩子),不受此限制.
- 使用KVO时,最好在 aspect_hookSelector: 调用之后添加观察者;否则可能会引起崩溃.
最后
Method Swizzling以及Runtime的一些特性就是iOS里的黑科技,如果能灵活应用的话可以在保证解决问题的前提下降低模块之间的耦合度,提高代码的可复用性。至于Method Swizzling与Aspect库的选择因人而异,我个人建议在最初阶段先放下Aspect而只用Method Swizzling原始代码去实现代码注入。掌握本质总是不吃亏的。