Aspects 实现原理

2,027 阅读6分钟

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 方法所在的类名。最后将当前实例selfisa指针指向这个子类,作用是当self接受消息时,会先在这个子类方法列表查找。

此时如果self收到的消息消息无法处理,会走forwardInvocation对应的实现,而前面这个方法已经被 hook 指向我们自己的实现,在我们自己实现中,通过NSInvocation能取出当前消息的接受对象,通过该对象从关联属性中取出 container 来执行。

现在的目标是如果通过调用被 hook 的方法,来触发forwardInvocation的调用。

2)处理被 hook 的实现 在 <objc/message.h> 头文件中有一个函数_objc_msgForward它的函数指针就是与 forwardInvocation关联的imp

只要我们将 hook 的方法与 _objc_msgForward实现交换,就能触发forwardInvocationimp了。

在调剂原selectorimp指向_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]调用时,触发的还是当前selfselector。也就导致了循环调用。

- (void)helloInstanceMethod {
    [super helloInstanceMethod]; // 导致 [self helloInstanceMethod]
}

对比总结

Aspects 在类层面上进行方法的调剂时,直接“原地”调剂forwardInvocation指向自己的实现,并将要 hook 的方法指向消息转发的实现。同时它必须保证同一条继承链上,不能 hook 同一个方法。

然而,对于实例对象的方法却不需要,因为实例方法的 hook 会动态的创建子类,并修改消息转发的实现,保证原类的其他实例方法不会受影响。