面向切面编程之 Aspects 源码解析及应用

1.前言

aop 编程(面向切面编程),其原理也就是在不更改正常的业务处理流程的前提下,通过生成一个动态代理类,从而实现对目标对象嵌入附加的操作。

iOS 中,要想实现相似的效果也很简单,利用 OC的动态性,通过 swizzling method 改变目标函数的 selector所指向的实现,然后在新的实现中实现附加的操作,完成之后再回到原来的处理逻辑。想明白这些之后,我就打算动手实现,当然并没有重复造轮子,我在 github发现了一个基于 swizzling method 的开源框架 Aspects 。这个库的代码量比较小,总共就一个类文件,使用起来也比较方便,比如你想统计某个controllerviewwillappear 的调用次数,你只需要引入 Aspect.h头文件,然后在合适的地方初始化如下代码即可。

- (void)addKvLogAspect {
    [self wr_Aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{
        KVLog_ReviewTimeline(ReviewTimeline_Open_Tab);
    }error:NULL];
}

这篇文章主要是介绍 Aspects 源码以及其思路,以及我在实际应用中遇到的一些问题。对 swizzling method不了解的同学可以先去网上了解一下,下面的内容是基于大家对swizzling method 有一定的了解的基础上的。

2. 基本原理

我们知道 OC 是动态语言,我们执行一个函数的时候,其实是在发一条消息:[receiver message],这个过程就是根据 message 生成 selector,然后根据 selector寻找指向函数具体实现的指针 IMP,然后找到真正的函数执行逻辑。这种处理流程给我们提供了动态性的可能,试想一下,如果在运行时,动态的改变了 selectorIMP 的对应关系,那么就能使得原来的[receiver message]进入到新的函数实现了。

那么具体怎么实现这样的动态替换了?

直观的一种方案是提供一个统一入口,如commonImp ,将所有需要 hook 的函数都指向这个函数,然后在这里,提取相关信息进行转发,JSPatch 实现原理详解对此方案的可行性有进行分析,对于64位机器可能会有点问题。另外一个方法就是利用 oc自己的消息转发机制进行转发,Aspects 的大体思路,基本上是顺着这个来的。为了更好的解释这个过程,我们先来看一下消息具体是怎么找到对应的imp 的,见下图。

[图片上传失败...(image-63d043-1546396164997)]

从上面我们可以发现,在发消息的时候,如果selector 有对应的 IMP ,则直接执行,如果没有,oc 给我们提供了几个可供补救的机会,依次有 resolveInstanceMethodforwardingTargetForSelectorforwardInvocation。Aspects 之所以选择在 forwardInvocation 这里处理是因为,这几个阶段特性都不太一样:resolvedInstanceMethod 适合给类/对象动态添加一个相应的实现,forwardingTargetForSelector 适合将消息转发给其他对象处理,相对而言,forwardInvocation 是里面最灵活,最能符合需求的。因此 Aspects 的方案就是,对于待 hook 的 selector,将其指向 objc_msgForward / _objc_msgForward_stret ,同时生成一个新的 aliasSelector 指向原来的 IMP,并且 hook 住 forwardInvocation 函数,使他指向自己的实现。按照上面的思路,当被 hook 的 selector 被执行的时候,首先根据 selector 找到了 objc_msgForward / _objc_msgForward_stret ,而这个会触发消息转发,从而进入 forwardInvocation。同时由于 forwardInvocation 的指向也被修改了,因此会转入新的 forwardInvocation 函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的 IMP。

3. 源码分析

3.1 数据结构

介绍完大致思路之后,下面将从代码层来来具体分析。从头文件中可以看到使用aspects有两种使用方式:1)类方法 2)实例方法

+ (id)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add((id)self, selector, options, block, error);
}

/// @return A token which allows to later deregister the aspect.
- (id)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add(self, selector, options, block, error);
}

两者的主要原理基本差不多,这里不做一一介绍,只是以实例方法为例进行说明。在介绍之前,先介绍里面几个重要的数据结构:

AspectOptions

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 执行的时机,也就是额外操作的执行时机,在我的应用场景中就是打点逻辑的执行时机,它可以在原始函数执行之前,也可以是执行之后,甚至可以完全替换掉原来的逻辑。

AspectsContainer

一个对象或者类的所有的 Aspects 整体情况

@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 的具体内容

// Tracks a single aspect.
@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

这里主要包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等

AspectInfo

一个 Aspect 执行环境,主要是 NSInvocation 信息。

@interface AspectInfo : NSObject 

- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;

@property (nonatomic, unsafe_unretained, readonly) id instance;

@property (nonatomic, strong, readonly) NSArray *arguments;

@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;

@end

3.2 代码流程

有了上面的了解,我们就能更好的分析整个 apsects 的执行流程。添加一个 aspect 的关键流程如下图所示:

[图片上传失败...(image-ab743e-1546397180123)]
从代码来看,要想使用 aspects ,首先要添加一个 aspect ,可以通过上面介绍的类/实例方法。关键代码实现如下:

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(^{
        //判断能否hook
        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;
}

这个过程基本和上面的流程图一致,这里重点介绍几个关键部分。

3.2.1 判断能否被 hook

对于对象实例而言,这里主要是根据黑名单,比如 retain forwardInvocation 等这些方法在外部是不能被hook ,(对于类对象还要确保同一个类继承关系层级中,只能被 hook 一次,因此这里需要判断子类,父类有没有被 hook,之所以做这样的实现,主要是为了避免出现死循环的出现,这里有相关的讨论)。如果能够 hook,则继续下面的步骤。

3.2.2 swizzling method

这是真正的核心逻辑,swizzling method 主要有两部分,一个是对对象的 forwardInvocation 进行 swizzling,另一个是对传入的 selector 进行 swizzling.

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    Class klass = aspect_hookClass(self, error);//1  swizzling forwardInvocation
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {//2  swizzling method
        // 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.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}

3.2.2.1 swizzling forwardInvocation:

aspect_hookClass 函数主要swizzling 类/对象的 forwardInvocation 函数,aspects 的真正的处理逻辑都是在 forwradInvocation 函数里面进行的。对于对象实例而言,源代码中并没有直接 swizzling 对象的 forwardInvocation 方法,而是动态生成一个当前对象的子类,并将当前对象与子类关联,然后替换子类的 forwardInvocation 方法(这里具体方法就是调用了 object_setClass(self, subclass) ,将当前对象 isa指针指向了 subclass ,同时修改了subclass以及其 subclass metaclassclass 方法,使他返回当前对象的 class。,这个地方特别绕,它的原理有点类似kvo 的实现,它想要实现的效果就是,将当前对象变成一个 subclass 的实例,同时对于外部使用者而言,又能把它继续当成原对象在使用,而且所有的swizzling操作都发生在子类,这样做的好处是你不需要去更改对象本身的类,也就是,当你在 remove aspects 的时候,如果发现当前对象的 aspect 都被移除了,那么,你可以将isa 指针重新指回对象本身的类,从而消除了该对象的 swizzling ,同时也不会影响到其他该类的不同对象)。对于每一个对象而言,这样的动态对象只会生成一次,这里 aspect_swizzlingForwardInvocation将使得 forwardInvocation方法指向 aspects 自己的实现逻辑 ,具体代码如下:

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    Class statedClass = self.class;
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);

    // Already subclassed
    if ([className hasSuffix:AspectsSubclassSuffix]) {
        return baseClass;

        // We swizzle a class object, not a single object.
    }else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
        // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
    }else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

    // Default case. Create dynamic subclass.
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
    Class subclass = objc_getClass(subclassName);
  //生成动态子类,并swizzling forwardInvocation方法
    if (subclass == nil) {
        subclass = objc_allocateClassPair(baseClass, subclassName, 0);
        if (subclass == nil) {
            NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
            AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
            return nil;
        }

        aspect_swizzleForwardInvocation(subclass);
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }
//将当前self设置为子类,这里其实只是更改了self的isa指针而已
    object_setClass(self, subclass);
    return subclass;
}
static void aspect_swizzleForwardInvocation(Class klass) {
    NSCParameterAssert(klass);
    // If there is no method, replace will act like class_addMethod.
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}

由于子类本身并没有实现 forwardInvocation ,隐藏返回的 originalImplementation 将为空值,所以也不会生成 NSSelectorFromString(AspectsForwardInvocationSelectorName)

3.2.2.2 swizzling selector

forwradInvocationhook 之后,接下来,将对传入的 selector 进行hook ,这里的做法是,将 selector 指向了转发 IMP ,同时生成一个aliasSelector ,指向了原来的IMP,同时为了放在重复 hook ,做了一个判断,如果发现 selector 已经指向了转发IMP ,那就就不需要进行交换了,代码如下

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    Class klass = aspect_hookClass(self, error);
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // 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.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}

3.2.3 handle ForwardInvocation

基于上面的代码分析知道,转发最终的逻辑代码最终转入 __ASPECTS_ARE_BEING_CALLED__ 函数的处理中。这里,需要处理的部分包括额外处理代码(如打点代码)以及最终重新转会原来的 selector 所指向的函数,其实现代码如下:

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {

...

     // Before hooks.  原来逻辑之前执行

    aspect_invoke(classContainer.beforeAspects, info);

    aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks.

    BOOL respondsToAlias = YES;

    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {//是否需要替换掉原来的路基

         aspect_invoke(classContainer.insteadAspects, info);

         aspect_invoke(objectContainer.insteadAspects, info);

    } else {

        Class klass = object_getClass(invocation.target);

        do {

              if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {

                    [invocation invoke];//根据aliasSelector找到原来的逻辑并执行

                    break;

                }

            }while (!respondsToAlias && (klass = class_getSuperclass(klass)));

     }

    // After hooks.  原来逻辑之后执行

     aspect_invoke(classContainer.afterAspects, info);

     aspect_invoke(objectContainer.afterAspects, info);

    // If no hooks are installed, call original implementation (usually to throw an exception)

     if (!respondsToAlias) {//找不到aliasSelector的IMP实现,没有找到原来的逻辑,进行消息转发
          invocation.selector = originalSelector;
          SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
          if ([self respondsToSelector:originalForwardInvocationSEL]) {
               ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
          } else {
              [self doesNotRecognizeSelector:invocation.selector];
         }
     }                     
...
}

依次处理 before/instead/after hook 以及真正函数实现。如果没有找到原始的函数实现,还需要进行转发操作。

4. 遇到的问题

以上就是 Apsects 的实现了,接下来会介绍在实际应用过程中遇到的一些问题以及我的解决方案。

你可能感兴趣的:(面向切面编程之 Aspects 源码解析及应用)