AOP(面向切面编程)

AOP,即Aspect Oriented Programming(面向切面编程),Gregor Kiczales(AOP的提出者)是这样介绍AOP的

We present an analysis of why certain design decisions have been so difficult to clearly capture in actual code. We call the proper-ties these decisions address aspects, and show that the reason they have been hard to capture is that they cross-cut the system’s basic functionality. We present the basis for a new programming technique, called aspect-oriented programming, that makes it possible to clearly express programs involving such aspects, including appropriate isolation, composition and re-use of the aspect code.

举一个最常见的例子,日志,无论我们做什么开发,总归离不开日志系统。而当工程逐渐变大时,大量的日志代码与逻辑代码混杂在一起,程序员不仅需要处理复杂的业务逻辑,还要兼顾日志。AOP即是把日志抽离出来,负责业务逻辑的程序员可以更专注于处理逻辑。对应到实际的iOS开发中,实现AOP最常见的方式即是利用Runtime的Method Swizzling特性将程序员已编辑好的逻辑代码加入指定行为构成新的方法再放回原来的位置。这样就可以神不知鬼不觉的在某个业务流的切面层面上添加新的行为。常见的例子除了日志还有权限控制,事务控制等。
Aspects是一个帮助开发者方便hook某个方法从而实现AOP,而避免亲自处理Method Swizzling等复杂的特性。

Aspects 源码解析

Aspects的API只有这两个

+ (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;

这两个接口是作者经过深思熟虑设计出来的,其中block是一个很特殊的地方,可以参考NSInvocation动态调用任意block
selector: 即是要hook的方法
options:定义如下,注释已经写的很明白了

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.
    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

block:即要插入的方法
error: 发生的错误

上面两个方法都会直接调用aspect_add:,区分类方法和实例方法是在aspect_hookClass:里进行的。
我们先整体的看下hook一个方法整体的流程图

我们按照这个流程一步一步来介绍。

aspect_add:方法
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    NSCParameterAssert(self);
    NSCParameterAssert(selector);
    NSCParameterAssert(block);

    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                [aspectContainer addAspect:identifier withOptions:options];

                // Modify the class to allow message interception.
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}

aspect_performLocked是使用OSSpinLockLock进行加锁来保证线程安全,AspectsContainer是一个绑定在对象上的一个容器,来存放aspect。AspectIdentifier即是一个简单的aspect,存放一些基础信息,比较有意思的是,其中还保存了block的方法签名,这个是为了生成NSInvocation对象,为的是实现动态调用block,block被允许是任意个数的参数,比较有意思,有兴趣的话可以看NSInvocation动态调用任意block

aspect_isSelectorAllowedAndTrack:方法

这个方法主要是防止一些黑名单内的方法(retain, release, autorelease, forwardInvocation)以及已经被hook过的方法被hook。

aspect_hookClass方法

调用aspect_prepareClassAndHookSelector方法方法后会先进入aspect_hookClass方法所以这里先介绍这个。
这个方法比较有意思,也比较难以理解。
总得来说,这个方法表现为

  • 已经生成子类,则返回该子类
  • class元类或被KVO过,则直接进入method swizzling
  • 其他情况则先生成class的一个子类,再将class的isa指针指向那个子类,并对该子类进行method swizzling

第一种情况不必多解释。如果baseClass是meta class的话,说明这个方法为类方法,就直接进入method swizzling。
比较隐晦的是下面这段代码

    Class statedClass = self.class;
    Class baseClass = object_getClass(self);
    ...
    else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

我们知道在OC Runtime机制里class方法被定义如下

- (Class)class {
    return object_getClass(self);
}

那么self.class和object_getClass(self)为何会不一样呢?原因其实出在KVO。
假如一个类被KVO过,会发生如下的事情,可以参考如何自己动手实现 KVO:
Apple会动态生成这个类的一个子类,然后重写被观察的属性的setter,当然这个setter还会调用之前的setter,这样就达到了观察的目的。为了不被使用者察觉,Apple将原来那个类的isa指针指向了这个子类,而且,Apple还重写了class方法以掩盖事实。
回到Aspects中来,假如一个类被KVO过,self.class是调用的原来的类的class方法,获取到原来的那个类,而object_getClass(self)则会通过isa指针(注意,被KVO过的类的isa指针已经指向了Apple生成的子类),获取到的自然是Apple生成的子类,这样一样,一旦这两个方法获取到的东西不是同一个,那就很明显被KVO过,也就触发了那个条件。
第三种情况会生成子类,而且生成过程的全程跟KVO生成子类如出一辙,也会跑去重写class方法掩盖事实。
除了第一种情况,剩下两种情况都会进入aspect_swizzleForwardInvocation方法,第一种情况其实是已经替换过了forwardInvocation的情况。意思就是无论怎么样,都至少调用过aspect_swizzleForwardInvocation方法。

aspect_swizzleForwardInvocation方法

这个方法其实很简单,就是将forwardInvocation:方法替换为ASPECTS_ARE_BEING_CALLED,这里我们先卖个关子,暂且不管这个方法,走通全部流程,我们再来看这个方法。

aspect_prepareClassAndHookSelector方法

下面我们要做的就是让这个类接收到该selector时,进入消息转发。回到aspect_prepareClassAndHookSelector方法,由aspect_hookClass方法得到一个类后,将该selector换为_objc_msgForward或_objc_msgForward_stret方法,并将原selector加上一个前缀重新添加到这个类中,也就说,当调用这个selector的时候,会强行进入消息转发,也就是会跳转进入forwordInvocation方法,由此就启动了消息转发,接应上了前面的aspect_swizzleForwardInvocation方法。

由此,整个流程已经走通,我们替换了原来的selector 改为objc_msgForward或_objc_msgForward_stret 强制进入了消息转发,消息转发启动后,由于置换了forwordInvocation为ASPECTS_ARE_BEING_CALLED,也就进入了ASPECTS_ARE_BEING_CALLED_方法。下面我们来看着一下这个方法

ASPECTS_ARE_BEING_CALLED

这个方法其实没有什么特殊的地方,就是用来执行被hook的selector以及插入的block

到此,整个hook的全过程已经走了一遍了,最终当系统调用该方法时,会走以下流程

解除hook的过程在这里就不细讲了,大概就是逆着走了一遍,有兴趣的朋友可以再做一些研究。

Tracks:

1. Class可作为NSDictionary/NSMutableDictionary的key

代码如下,取出对象

Class currentClass = [self class];

AspectTracker *tracker = swizzledClassesDict[currentClass];

放入对象

tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
                swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;

NSDictionary/NSMutableDictionary在设置对象的时候是传入一个KeyType对象,KeyType要求对象实现NSCopying且不为空。Apple的官方文档上关于NSObject的copyWithZone:的介绍

This method exists so class objects can be used in situations where you need an object that conforms to the NSCopying protocol. For example, this method lets you use a class object as a key to an NSDictionary object. You should not override this method.

大概是说,NSObject实现了copyWithZone:方法(即实现了NSCopying协议),这样带来便利,比如可以使用一个Class对象来作为NSDictionary的key

2.do/while(0)模式
3.Conditionals with Omitted Operands

代码如下

self.beforeAspects  = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break;

使用了self.beforeAspects ?:@[]这句话等价于self.beforeAspects ? self.beforeAspects:@[],这其实类似于Swift中的??运算有兴趣可以看下空合运算符 Swift vs Objective-C

Preference

[1] Kiczales G, Lamping J, Mendhekar A, et al. Aspect-oriented programming[M]//ECOOP'97—Object-oriented programming. Springer Berlin Heidelberg, 1997: 220-242.
[2] Method Swizzling 和 AOP 实践
[3] Aspects 源代码解析<一>