Aspects 是 Objective-C 比较知名的 AOP 框架,实现方法调剂(method swizzling)。通过使用 Aspects 提供的接口,比直接使用 runtime 提供的接口,更加方便灵活。
Aspects 现在不建议在生产环境使用,但它的实现原理,还是非常值得学习和借鉴的。
Aspects 支持实例和类的方法的调剂,虽然内部调用的是同一个方法,在实现上有较大的差别。
实例的方法交换
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add(self, selector, options, block, error);
}
- aspect_hookSelector
内部调用的aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error)
由 C 函数实现。该函数有五个参数:
self
:当前调剂方法的对象;selector
:被调剂的方法名;options
:调剂的方式类型为 AspectOptions;block
:调剂selector
方法是需要混入执行的 block;error
: 调剂过程发生的错误信息;
AspectOptions 有三种类型,用于决定 block 和原方法执行的顺序,分别有在原方法之前、之后、或者直接替换。
我们逐一看看 aspect_add 函数的内部实现:
__block AspectIdentifier *identifier = nil;
// 1
aspect_performLocked(^{
// 2
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
// 3
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
// 4
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
// 5
[aspectContainer addAspect:identifier withOptions:options];
// 6
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
1、 为方法调剂加锁,保证线程安全,使用的是自旋锁,然而 OSSpinLockLock 已不再安全。
2、
判断当前 selector 是否支持调剂。如果被调剂方法是 retain, release, autorelease, forwardInvocation:
则返回错误。forwardInvocation
方法不支持调剂是因为 Aspects 的实现基于这个方法,后面会讲到。至于其他几个内存管理的方法,笔者不是很确定不能调剂的原因,可能是在这些方法中对象处于不稳定状态, 访问当前对象存在安全隐患。
接着是保证调剂dealloc
方法只能是AspectPositionBefore
类型,dealloc 负责释放资源,不能被替换,执行后对象释放不能再执行 block 操作。
最后判断当前对象能否响应被调剂的方法。
3、
aspectContainer
以关联对象的形式作为self
的属性,属性的名称为 selector
加前缀aspects_
。
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
AspectsContainer
类包含三个数组属性,分别用于保存在 block 三种执行的情况。
4、
AspectIdentifier
以对象的形式封装调剂信息,其中包含方法名,执行的对象,执行时机,以及执行的 block。
5、 将 aspect identifier 存储到当前对象关联的 container 中。在方法被调用的时候,会遍历这个 container 中所有的 identifier,根据它包含的信息来执行。
6、 这是最关键的一步,也是 Aspects 实现的核心所在。主要分为两部分,
1)处理被 hook 方法的对象所属的类
Class aspect_hookClass(NSObject *self, NSError **error)
如果对象所属的类已经被 Aspects 处理过就直接返回,否则该函数为当前实例对象所属的类,动态的创建一个子类,并 hook 该子类的forwardInvocation
方法,指向自己的__ASPECTS_ARE_BEING_CALLED__
函数实现。
重置该子类对象和元类的 class 方法,使其返回(父类)被 hook 方法所在的类名。最后将当前实例self
的isa
指针指向这个子类,作用是当self
接受消息时,会先在这个子类方法列表查找。
此时如果self
收到的消息消息无法处理,会走forwardInvocation
对应的实现,而前面这个方法已经被 hook 指向我们自己的实现,在我们自己实现中,通过NSInvocation
能取出当前消息的接受对象,通过该对象从关联属性中取出 container 来执行。
现在的目标是如果通过调用被 hook 的方法,来触发forwardInvocation
的调用。
2)处理被 hook 的实现
在 <objc/message.h> 头文件中有一个函数_objc_msgForward
它的函数指针就是与 forwardInvocation
关联的imp
。
只要我们将 hook 的方法与 _objc_msgForward
实现交换,就能触发forwardInvocation
的imp
了。
在调剂原selector
的imp
指向_objc_msgForward
函数之前,需要先判断该 selector 是否已经被调剂过,如果已经是_objc_msgForward
的实现就不作处理。
如果不是,我们要对selector
的实现做交换了,假如我们直接将其与 _objc_msgForward
交换,如果对象的另一个selector
也做 hook,就会把前一个selector
的实现给覆盖了。所以这里动态的添加一个 aliasSelector
方法用于保存原selector
的实现,再用_objc_msgForward
函数替换原 selector 的实现。
当被调剂的方法执行时,会执行动态创建的子类的forwardInvocation
方法,而在1)
中我们讲到该方法的实现已经被 hook,指向了 __ASPECTS_ARE_BEING_CALLED__
函数。
__ASPECTS_ARE_BEING_CALLED__
函数的实现其实并不复杂,它从 forwardInvocation
的参数NSInvacation
中取出触发消息转发的 selector,将它替换为aspects_
前缀的别名selector
,因为这个别名 selector
指向前面保存的原selector
的实现。
接着从self
中取出使用以别名selector
作为属性的关联对象 AspectsContainer
,container 中包含selector
方法被调剂的信息集合。先执行 before 的 block,接着执行封装在invocation
中的原实现,最后执行 after 类型的 block。如果有 instead 类型的 block 则原实现不会被执行。
其中 block 并不是直接执行 aspect 中保存的那个,而是对其参数进行了处理,在第一个参数中插入 AspectInfo 类型的实例。
类的方法交换
类的方法交换流程与实例的方法交换大致相同,主要区别在于对同一继承链上的类 hook 相同的方法进行了限制。
实例和类调用的都是aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error)
函数,所以 self
参数可能是实例,也可能是类,源码中使用 object_getClass
,如果是实例对象会返回类,如果是类会返回元类,再通过class_isMetaClass
判断self
是否是为类。
在aspect_isSelectorAllowedAndTrack
函数中,对类的情况做了一大堆处理,其实就做一件事情:判断当前self
所在的继承链是否有同名的 selector
已经被 hook 过了。
如果没有,就记录当前self
的继承关系,直到基类NSObject
。
之所以不能 hook 具有继承链关系同名方法,是因为如果子类方法调用super
会导致死循环。
关键原因在于super
关键字的实现,它是编译器的一个助记符,在方法调用时,会在父类的方法列表中查找,但不管在哪一层级找到,消息的接受者仍然是当前类/实例。也就是说在执行super
方法时,如果父类的该方法也被调用,就会走__ASPECTS_ARE_BEING_CALLED__
函数,该函数中的 NSInvocation
参数中的 target
依然是self
,这就导致[invocation invoke]
调用时,触发的还是当前self
的selector
。也就导致了循环调用。
- (void)helloInstanceMethod {
[super helloInstanceMethod]; // 导致 [self helloInstanceMethod]
}
对比总结
Aspects 在类层面上进行方法的调剂时,直接“原地”调剂forwardInvocation
指向自己的实现,并将要 hook 的方法指向消息转发的实现。同时它必须保证同一条继承链上,不能 hook 同一个方法。
然而,对于实例对象的方法却不需要,因为实例方法的 hook 会动态的创建子类,并修改消息转发的实现,保证原类的其他实例方法不会受影响。