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

1. 背景

(这篇文章在微信读书Weread团队博客中也有发表)ios

最近在作项目的打点统计的时候,发现业务逻辑和打点逻辑常常耦合在一块儿,这样一方面影响了正常的业务逻辑,同时也很容易搞乱打点逻辑,并且要查看打点状况的时候也很分散,所以想着如何将二者解耦,并将打点逻辑集中起来。其实在 web 编程时候,这种场景很早就有了很成熟的方案,也就是所谓的 aop 编程(面向切面编程),其原理也就是在不更改正常的业务处理流程的前提下,经过生成一个动态代理类,从而实现对目标对象嵌入附加的操做。在 ios 中,要想实现类似的效果也很简单,利用 oc 的动态性,经过 swizzling method 改变目标函数的 selector 所指向的实现,而后在新的实现中实现附加的操做,完成以后再回到原来的处理逻辑。想明白这些以后,我就打算动手实现,固然并无重复造轮子,我在 github 发现了一个基于 swizzling method 的开源框架 Aspects 。这个库的代码量比较小,总共就一个类文件,使用起来也比较方便,好比你想统计某个 controller 的 viewwillappear 的调用次数,你只须要引入 Aspect.h 头文件,而后在合适的地方初始化以下代码便可。git

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

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

2. 基本原理

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

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

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

流程图

从上面咱们能够发现,在发消息的时候,若是 selector 有对应的 IMP,则直接执行,若是没有,oc 给咱们提供了几个可供补救的机会,依次有 resolveInstanceMethod、forwardingTargetForSelector、forwardInvocation。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有两种使用方式app

  1. 类方法框架

  2. 实例方法jsp

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

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)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 总体状况

// Tracks all aspects for an object/class.
@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 的具体内容

@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)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 <AspctInfo>
- (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 的关键流程以下图所示:

这里写图片描述

从代码来看,要想使用 aspects ,首先要添加一个 aspect ,能够经过上面介绍的类/实例方法。关键代码实现以下:

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    ...
    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {//1判断可否hook
            ...//2 记录数据结构
            aspect_prepareClassAndHookSelector(self, selector, error);//3 swizzling
        }
    });
    return identifier;
}

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

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

2)swizzling method:这是真正的核心逻辑

3.2.1 swizzling method

swizzling method 主要有两部分,一个是对对象的 forwardInvocation 进行 swizzling,另外一个是对传入的 selector 进行 swizzling.

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    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
       ...//
    }
}
3.2.1.1 swizzling forwardInvocation

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

static Class aspect_hookClass(NSObject *self, NSError **error) {
     ...
     //生成动态子类,并swizzling forwardInvocation方法
     subclass = objc_allocateClassPair(baseClass, subclassName, 0); 
     aspect_swizzleForwardInvocation(subclass);//swizzling forwardinvation方法

     objc_registerClassPair(subclass);
      ...
     object_setClass(self, subclass);//将当前self设置为子类,这里其实只是更改了self的isa指针而已
     return subclass;
}
...
static void aspect_swizzleForwardInvocation(Class klass) {
     ...
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:),     (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
         class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName),        originalImplementation, "v@:@")
      }
...
}

因为子类自己并无实现 forwardInvocation ,隐藏返回的 originalImplementation 将为空值,因此也不会生成 NSSelectorFromString(AspectsForwardInvocationSelectorName) 。

3.2.1.2 swizzling selector

当 forwradInvocation 被 hook 以后,接下来,将对传入的 selector 进行 hook ,这里的作法是,将 selector 指向了转发 IMP ,同时生成一个 aliasSelector ,指向了原来的 IMP ,同时为了放在重复 hook ,作了一个判断,若是发现 selector 已经指向了转发 IMP ,那就就不须要进行交换了,代码以下

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
     ...
     Method targetMethod = class_getInstanceMethod(klass, selector);
     IMP targetMethodIMP = method_getImplementation(targetMethod);
     if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
     ...
     SEL aliasSelector = aspect_aliasForSelector(selector);//generator aliasSelector
     if (![klass instancesRespondToSelector:aliasSelector]) {
          __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); 
     }
     class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);// point to   _objc_msgForward
   ...
     }
}

3.2.2 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 的实现了,接下来会介绍在实际应用过程当中遇到的一些问题以及个人解决方案。

4.1 jspatch不兼容

缘由

咱们的项目中引入了 jspatch 做为咱们的 hot fix方案。 jspatch 也会 hook 住对象的 forwradInvocation 方法,而且 swizzling 相应的 method,使其指向转发 IMP,因为 aspects 也是基于这二者实现的,那么会不会致使问题呢(其实相似的问题也会发生在对象提早被 kvo 了,会不会有影响)?

回过头去看3.2.1 咱们先是 hook了 类的 forwardInvocation 使其指向了 __ASPECTS_ARE_BEING_CALLED__,而后在 swizzling method 那里, aspect 有作一个判断,若是传入的 selector 指向了转发 IMP,那么咱们什么也不作。所以可想而知,若是传入的 selector 先被 jspatch hook,那么,这里咱们将不会再处理,也就不会生成 aliasSelector 。

这会致使什么问题了?设想一下,当 selector 被触发的时候,因为 selector 指向了转发 IMP,所以会进入消息转发过程,同时因为 forwardInvocation 被 aspects 所 hook,最终会进入到 aspects 的处理逻辑 ASPECTS_ARE_BEING_CALLED 中来。让咱们回过头去看看3.2.2中的分析,因为找不到 aliasSelector 的 IMP 实现,所以会在此进行消息转发。而在3.2.1.1的分析中咱们知道,子类并无实现 NSSelectorFromString(AspectsForwardInvocationSelectorName),因此这里的流程就会进入 doesNotRecognizeSelector,从而抛出异常。

解决方案

出现上诉问题的缘由在于,当 aliasSelector 没有被找到的时候,咱们没能将消息正常的转发,也就是没有实现一个 NSSelectorFromString(AspectsForwardInvocationSelectorName),使得消息有机会从新转发回去的方法。所以解决方案也就呼之欲出了,个人作法是在对子类的 forwardInvocation 方法进行交换而不只仅是替换,实现逻辑以下,强制生成一个 NSSelectorFromString(AspectsForwardInvocationSelectorName) 指向原对象的 forwardInvocation 的实现。

static Class aspect_hookClass(NSObject *self, NSError **error) {
          ...
         subclass = objc_allocateClassPair(baseClass, subclassName, 0);
         ...
         IMP originalImplementation = class_replaceMethod(subclass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
         if (originalImplementation) {
              class_addMethod(subclass, NSSelectorFromString(AspectsForwardInvocationSelectorName),   originalImplementation, "v@:@");
          } else {
              Method baseTargetMethod = class_getInstanceMethod(baseClass, @selector(forwardInvocation:));
              IMP baseTargetMethodIMP = method_getImplementation(baseTargetMethod);
             if (baseTargetMethodIMP) {
                     class_addMethod(subclass, NSSelectorFromString(AspectsForwardInvocationSelectorName), baseTargetMethodIMP, "v@:@");
               }
        }
...
}

注意若是 originalImplementation 为空,那么生成的 NSSelectorFromString(AspectsForwardInvocationSelectorName) 将指向 baseClass 也就是真正的这个对象的 forwradInvocation,这个其实也就是 jspatch hook 的方法。同时为了保证 block 的执行顺序(也就是前面介绍的 before hooks / instead hooks / after hooks ),这里须要将这段代码提早到 after hooks 执行以前进行。这样就解决了 forwardInvocation 在外面已经被 hook 以后的冲突问题。

4.2 remove操做

4.2.1 单个aspect remove

单个 aspect 的 remove 貌似有个问题,先来看看源码。

if (aspect_isMsgForwardIMP(targetMethodIMP)) {
      SEL aliasSelector = aspect_aliasForSelector(selector);
      Method originalMethod = class_getInstanceMethod(klass, aliasSelector);
      IMP originalIMP = method_getImplementation(originalMethod);
      if (originalIMP) {            
            class_replaceMethod(klass, selector, originalIMP, typeEncoding);
      } 
}

当你对某个 aspect 执行 remove 操做的时候,它会直接 replace 这个 selector 的 IMP,这个操做是对整个类的全部实例都生效的,这会致使什么问题呢?

以类 A 为例,你先进入了 A 的一个实例 A1,hook 住了方法 selector1,而后,并无销毁这个实例的时候,经过其余路径又进入类 A 的另外一个实例 A2,固然也 hook 了 selector1,而后这个时候,若是你 A2 中执行了这个 aspect 的 remove 操做,按照上面的逻辑,类 A 的 selector1 将会恢复正常,可像而知,当你退回 A1 的时候, A1 的 aspect 将会失效。这里其实个人解决思路很简单,由于在执行 remove 操做的时候,其实和这个对象相关的数据结构都已经被清除了,即便不去恢复 selector1 的执行,在进入 ASPECTS_ARE_BEING_CALLED 因为这个没有响应的 aspects,其实会直接跳到原来的处理逻辑,并不会有其余附加影响。

4.2.2 整个对象aspects remove

还有一个问题就是, aspects 的 remove 操做只能支持单个的 remove 操做,不支持一次性删除一个对象的全部 aspects 。这里,也作了一个扩展,对原来的 aspects 进行扩展,实现了一次性 remove 一个对象全部 aspects 的方法。

相关文章
相关标签/搜索