Aspects 源码分析

Aspects 源码分析

AOP 作用

面向对象,面向过程,是软件开发同学倒背如流的概念了,工作中也无时无刻不在与 OOP, POP 打交道。OOP 和 POP 原本就谈不上孰优孰劣的是非问题了,各有千秋,对于 AOP 也是同样。

OOP, POP, AOP 这几个概念其实代表着解决问题不同视角,角度不同解决问题的方法和手段也各异。面向对象,通过对事物分类归一的方式解决问题;面向过程,通过对问题划分步骤,分阶段,一步一步的解决问题,但对于问题的规模有着一定的局限性;面向切面,有着更加独特的思维方式,它会把某个事务拦腰切断,在这个被切断的断面,像做外科手术一样,对这个切断面做一些 OOP和POP 都很难做到的工作,同时还不会影响事物原有的特性。

下面我们通过 Aspects 来剖析一下AOP在 Objective-C 系统中的实现方式,不同的平台实现方式不尽相同,但是实现目标基本一致。

Aspects 核心代码介绍

实现逻辑简介

简单来说分四步:
1. 添加切面到切面容器,并和目标对象完成关联。(见代码 aspect_add()[aspectContainer addAspect:identifier withOptions:options];)
2. 提供新的 forwardInvocation: 实现函数,并在该函数中完成,各个切面的处理调用,以及目标方法的调用。 (见代码 aspect_prepareClassAndHookSelectoraspect_hookClass)
3. 将原有的方法改名。 (见代码aspect_prepareClassAndHookSelector__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); 处。)
4. 将原有的选择器映射到 _objc_msgForward。 (见代码aspect_prepareClassAndHookSelectorclass_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); )

关键代码分析

Aspects 的源码规模不大,但是各种函数,方法,类,也不算少。我们还是需要从添加切面的调用过程入手,理解 Aspects 的切面实现过程,下面我们就从添加切面的入口方法开始分析吧

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

首先该方法是 NSObject 的类别,所以可以为任何类的 selector 挂载合适的切面处理块(block)

该方法有类方法和实例方法两个版本,分别用来为类方法和实例方法添加切面

/*
该方法会通过 aspect_getContainerForObject 向目标对象(类对象或者实例对象)添加 AspectsContainer 类型的关联对象,用来作为切面信息的容器。

随后将 block 封装到 AspectIdentifier 对象中,将 AspectIdentifier 对象添加到 AspectsContainer 容器实例中。这个过程可以简单理解为,保存切面处理块。

最关键的地方是: aspect_prepareClassAndHookSelector 函数,通过运行时修改当前类,并完成消息拦截工作。这里暂且简单交代一下,后面我们详细解释 aspect_prepareClassAndHookSelector 进行拦截的过程。

*/
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error);

/*
通过  aspect_hookClass  挂载对应类的实例方法或类方法

*/
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    Class klass = aspect_hookClass(self, error);// 挂载对应类的实例方法或类方法,调和 forwardInvocation: 方法,用 __ASPECTS_ARE_BEING_CALLED__ 替换原始实现。并将原始实现转移到新增的 "__aspects_forwardInvocation:" 选择器中。 另外如果是类对象,因为没有转发调用,所以为添加类的转发调用。同时将调和过的类,加入缓存中以备今后检查。
    Method targetMethod = class_getInstanceMethod(klass, selector);//获取类的选择器方法对象
    IMP targetMethodIMP = method_getImplementation(targetMethod); //获取对应的方法实现
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {//只替换 _objc_msgForward 以外的函数,因为我们还需要使用 _objc_msgForward 
        // Make a method alias for the existing method implementation, it not already copied.
        const char *typeEncoding = method_getTypeEncoding(targetMethod); //获取方法类型编码
        SEL aliasSelector = aspect_aliasForSelector(selector); //为选择器生成别名
        if (![klass instancesRespondToSelector:aliasSelector]) { //将选择器的原始实现映射到新的别名,以后还原时可用
            __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
            NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
        }

        // We use forwardInvocation to hook in. 这里已经说得很清楚了,使用 forwardInvocation 替换选择器的实现,forwardInvocation: 的运行时实现是 _objc_msgForward_stret / _objc_msgForward,而下边的  aspect_getMsgForwardIMP() 函数对选择哪个消息转发函数进行了判断。_objc_msgForward_stret 的后缀 stret 的含义是 struct return,表示该函数返回值是结构体。
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}

最后总结一下实现机制

关键元素:

AspectIdentifier, AspectsContainer, forwardInvocation:, _objc_msgForward

AspectIdentifier

AspectIdentifier 是切面处理块的包装类,它的实例就是一个切面。提供了切面处理的相关信息,包括对应的目标 selector, 处理块,块的方法签名,目标对象,以及切面类型(before, instead, after 分别对应了执行前,替换执行体,执行后三种不同的切面动作)

AspectsContainer

由于对同一个方法的同一个切入点会有多个切面处理,为了支持多个切面处理,切面容器应运而生。

@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

看代码很容易理解,三种不同的切入点对应了三个数组,用来容纳多个切面单元(AspectIdentifier 实例)

aspect_getContainerForObject 函数会将切面容器作为关联对象添加到目标对象中,对外隐藏实现,每次添加切面,都自动向该切面容器中加入新的切面。

forwardInvocation:

转发调用(forwardInvocation:): 将 选择器 forwardInvocation: 指向我们定义的新函数 void impAOPForwardInvocation(id obj, SEL selector, NSInvocation *invocation) ,代替默认实现

_objc_msgForward, _objc_msgForward_stret

消息转发_objc_msgForward: 将目标方法的实现指向该函数,带来的结果是,每当发送目标方法的消息时,runtime 不再调用目标方法的实现函数,而是直接调用消息转发函数,间接调用 forwardInvocation:,进而调用我们提供的 impAOPForwardInvocation 函数,由于 runtime 把函数调用通过 NSInvocation 封装起来了,我们直接进行 invoke 调用即可。

整体逻辑

Aspects 首先将切面(处理块),添加到对应的切面容器(AspectContainer)中,同时将该容器设置为目标对象的关联对象。

然后将目标选择器的方法实现,替换为 _objc_msgForward_objc_msgForward_stret。目的是,每次的方法调用,都能直接调用 forwardInvocation: 方法、

目标方法,在 aspect_prepareClassAndHookSelector 函数中,通过 class_addMethod 改名为 aspects_selector

而我们的重头戏在 aspect_prepareClassAndHookSelector 中通过 aspect_hookClass ,将 forwardInvocation: 替换成了 Aspects 提供的实现 __ASPECTS_ARE_BEING_CALLED__, 在 __ASPECTS_ARE_BEING_CALLED__ 完成了各个切面调用和目标方法的调用。

重点:
由于目标方法的调用参数,以及签名并未发生改变,依然是原有的选择器,消息发送方和原有的参数。我们知道 runtime 在消息转发时会将消息发送对象,函数调用,参数以及返回值都封装到 NSInvocation 中,因此在 __ASPECTS_ARE_BEING_CALLED__ 实现中我们替换掉 anInvocation.selector,使它指向被重命名的目标方法,直接使用 [anInvocation invoke] 完成调用就能完成目标函数的调用。

发散一下

逻辑借鉴 Aspects,通过前面说的四个步骤,我们也能很快实现一个 AOP 框架。但是 Aspects 还有很多实现细节需要我们注意和学习的,比如:对于 block 签名的计算,通过局部静态变量实现函数的模块化封装,切面生命周期的管理:清除,添加,恢复等,选择器黑名单,通过 AspectTracker 管理跟踪被修改的选择器状态。