面向切面编程Aspects源码解析

5次阅读

共计 9952 个字符,预计需要花费 25 分钟才能阅读完成。

面向切面编程

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

简单说,就是在不影响我们现在正常业务的情况下,对某些类的某些方法嵌入操作。我们可以很通俗的理解一个方法可以有方法前和方法后这两个切面,当然还可以把方法执行过程看过一个整的切面去 hook。

在我们的 iOS 开发中,AOP 的实现方法就是使用 Runtime 的 Swizzling Method 改变 selector 指向的实现,在新的实现中添加新的操作,执行完新实现之后,再处理之前的实现逻辑。

Aspects

Aspects 是 iOS 平台比较成熟的 AOP 的框架,这次我们主要来研究一下这个库的源码。

基于 Aspects 1.4.1 版本。

总览

Aspects 给出了两个方法,一个类方法一个实例方法,使用起来非常简单。

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

传参也很容易理解,selector 自然就是我们要 hook 的方法,options 使我们要 hook 的位置,下面具体再说,block 是一个回调,也就是我们所说的要嵌入的代码逻辑,error 就是 hook 失败,当然了失败的原因较多,我们下面会提到。

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.
};

options 也是一个枚举的类型,看一下里面定义的字段就很容易明白了,AspectPositionAfter 是表示嵌入的放大要在被 hook 方法原来逻辑之前之后执行,AspectPositionBefore 是之前执行,AspectPositionInstead 表示要用嵌入的代码替换掉之前的逻辑,AspectOptionAutomaticRemoval 表示 hook 执行后,移除 hook。

因为是 NS_OPTIONS 类型,可多选。

重要的类

除了上述核心的方法是通过 NSObject 的 Category 的方式给出,还有以下几个类比较重要。

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

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

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

核心方法 aspect_add

Aspects 给出的两个方法最终都是调用了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(^{
        // 是否允许 hook -
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {// 取到 sel 对应的 container (一个类不同 sel 对应不同的 container?)
            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;
}
__block AspectIdentifier *identifier = nil;

每次在添加 hook 的时候,都会先创建一个 AspectIdentifier。
__block 是为了能在下面的 block 中修改 identifier。

aspect_performLocked是封装了一个自旋锁。

在自旋锁中会有一个 if 语句来判断 selector 是否能被 hook。那我们就先来看一下是否能被 hook 的判断方法aspect_isSelectorAllowedAndTrack

aspect_isSelectorAllowedAndTrack
  1. 黑名单

    aspect_isSelectorAllowedAndTrack方法中维护了一个 NSSet,在初始化的时候加入了一些方法名,在源码中是下面这些。

    [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];

    这说明了后面这几个方法是不允许被 hook 的,如果 hook 了这些方法会有错误的信息提示。

  2. dealloc 方法

    在 hook 的方法中,dealloc 属于一个特殊情况,因为这个方法是在对象要被销毁的时候创建,所以 Aspects 为了安全起见,在 hook dealloc 方法的时候。options 只允许时 AspectPositionBefore,也就是插入的逻辑只能在 dealloc 原有逻辑之前处理,不允许替换或者在 dealloc 之后。

  3. 没找到方法

    这个就没啥疑问了,如果我们 hook 了一个根本不存在的方法也会有错误提示。

  4. 方法只允许 hook 一次(元类相关)

    这个有点麻烦,因为从错误提示的枚举来看,他是对应 AspectErrorSelectorAlreadyHookedInClassHierarchy 这一项的,从字面意思来看是说方法已经被 hook 了。

    最开始我对这个类的层级不是很明白,我的最初理解的类的层级是父类和子类不能同时 hook,经过验证这种理解是错误的。

    后来我仔细地看了一下源码,他的元类判断中是这么写的:

     if (class_isMetaClass(object_getClass(self))) {}

    判断的是 object_getClass(self),通过 runtime 的源码我们可以知道,object_getClass 得到的是传参的 isa 指针指向的结构,意识就是 self 如果是对象,object_getClass(self)得到的是对应的类,如果 self 是类,那就得到了元类。

    那什么时候会提示这个错误呢,我举一个例子吧。我创建了一个 TestHookViewController 类,继承自 UIViewController,如果我在TestHookViewController 中像下面这样写就会有错误提示了。

    [[UIViewController class] aspect_hookSelector:@selector(viewWillAppear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) {NSLog(@"%s",__func__);
    } error:NULL];
    
    [[TestHookViewController class] aspect_hookSelector:@selector(viewWillAppear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) {NSLog(@"%s",__func__);
    } error:NULL];

    当然了,这两个 hook 的前后位置不同,打印台输出的提示也是不一样的,虽然都是一个类层级方法只允许 hook 一次的错误原因。这个大家自行尝试一下。

    if 语句里面就是关于方法是否重复 hook 的判断逻辑,这里牵扯到一个相关类,AspectTracker。我们现在就来看一下这个类。

AspectTracker 类

虽然是说 AspectTracker 类,但是代码结构还是接着上面的咱们说到的位置继续往下走。

因为 AspectTracker 主要就是用在方法只允许 hook 一次的判断中。

Aspects 维护了一个字典,来储存被 hook 方法的类和对应的 AspectTracker。

Class currentClass = [self class];
AspectTracker *tracker = swizzledClassesDict[currentClass];

通过上面的方式取到对应的 AspectTracker。这里提一句,这里的代码我们应该先看下面的,要先了解 AspectTracker 是怎么存储到字典里面的。

这里我们要看下面这个 do-while 循环

currentClass = klass;
AspectTracker *subclassTracker = nil;
do {tracker = swizzledClassesDict[currentClass];
    if (!tracker) {tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
        swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
    }
    if (subclassTracker) {[tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
    } else {[tracker.selectorNames addObject:selectorName];
    }

    // All superclasses get marked as having a subclass that is modified.
    subclassTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));

如果最开始没有 tracker,会初始化一个,然后存到字典中。最开始的时候 subclassTracker 为 nil,所以 selector 会 add 到 tracker.selectorNames。

然后 currentClass 重新赋值

currentClass = class_getSuperclass(currentClass)

再次执行 do 逻辑里面的代码,这次 subclassTracker 就有值了(上一次循环的 tracker),就会执行[tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];

我们进到 addSubclassTracker 源码中可以看到,tracker 被存到 selectorNamesToSubclassTrackers 这个字典中,关键的是这个字典的 key 是 selectorName,value 是一个集合,tracker 是放在这个集合里面的。为什么要通过集合来存 tracker 呢?

因为这里是有子类的情况的,一个类的子类可能有多个,如果在不同的子类中 hook 了这个父类的一个方法,也就是父类中的这一个 selector 被多次 hook,所以也会有不同的 tracker,所以使用一个集合来储存。

其实说到这个地方大家就差不多可以理解了,如果满足了
if (class_isMetaClass(object_getClass(self))) 这个判断,我们会把这个类 hook 的方法通过封装为 AspectTracker 来进行记录,当然包括他的层层父类,都对对应一个AspectTracker,而且父类中的还会记录子类中 hook 的方法。这部分代码最好是 debug 跟一下,会明显一点。

上面我们先看了 tracker 是怎样被存起来的,接来下再来看关于只能被 hook 一次的判断。

首先要判断子类中是否 hook

if ([tracker subclassHasHookedSelectorName:selectorName]) {// 内部省略}

subclassHasHookedSelectorName内部实现很简单

- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName {return self.selectorNamesToSubclassTrackers[selectorName] != nil;
}

就是查询一下 selectorNamesToSubclassTrackers 这个字典中,通过 seletorName 是否能取到值,上面已经说过了,这个字典中通过 key:selectorName value: set 的方法储存了子类的 tracker。

如果能取到值,就说明子类中已经 hook 了这个方法了,父类中就不能在 hook。

如果没能取到值,说明当前类可能就是个子类,此时需要看一下他的父类中是否 hook 了这个 selector,所以就会执行下面的的 do-while 循环。此处的代码就不展示了。

但这个位置初步的关于方法能否被 hook 就已经判断完了。如果可以 seletor 可以被 hook,继续 if 里面的代码。

Swizzling Method

我们直接说 Swizzling Method 这一最重要的逻辑吧。

Swizzling Method 主要有两部分,一个是对对象的 forwardInvocation 进行 swizzling,另一个是对传入的 selector 进行 swizzling。

我们来看 aspect_prepareClassAndHookSelector 方法的源码。

替换 forwardInvocation

首先是Class klass = aspect_hookClass(self, error);

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

 //
 // 省略一部分代码
 //
 //
    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;
        }
        // 替换 forwardInvocation 方法
        aspect_swizzleForwardInvocation(subclass);
        // subclass 的 class 方法交替换  替换为 statedClass 的 class 方法   subclass 元类也替换
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }
    // 改变当前类的 isa 指针指向
    object_setClass(self, subclass);
    return subclass;
}

我们从源码中可以看出逻辑,主要是动态创建了一个 subclass,名为 subclass,其实最终把我们 hook 的类的 isa 指针指向了这个 subclass,实为是一个父类。

aspect_swizzleForwardInvocation(subclass);

上面这个方法是替换了 forwardInvocation: 方法。

当然了,也是替换的这个 subclass 的 forwardInvocation: 方法,把 forwardInvocation: 替换为 __ASPECTS_ARE_BEING_CALLED__ 这个方法,主要的 hook 后的代码执行处理逻辑都在这个 __ASPECTS_ARE_BEING_CALLED__ 中。

在替换了 aspect_hookClass 方法之后,同时修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回当前对象的 class。这个地方有点绕,其实最终目的就是把所有的 swizzling 都放到了这个 subclass 中处理,不影响原来的类,而且对于外部的使用者,又可以把它当做原对象使用。

替换 selector

执行完 aspect_hookClass 完之后,forwardInvocation:方法已经被替换,下面会执行 swizzling selector 的代码。

在 swizzling selector 的时候,将 selector 指向了消息转发 IMP,同时生成一个 aliasSelector,指向原方法的 IMP。

这里代码就不往外粘了。

处理 forwardInvocation

其实上面已经把整个过程分析完了,我们也知道,最后转发的代码最终会在 __ASPECTS_ARE_BEING_CALLED__ 函数的处理中。所以最后我们来看看这个函数就可以了。

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {NSCParameterAssert(self);
    NSCParameterAssert(invocation);
    SEL originalSelector = invocation.selector;
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    invocation.selector = aliasSelector;
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
    NSArray *aspectsToRemove = nil;

    // 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) {
        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];
        }
    }

    // Remove any hooks that are queued for deregistration.
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}

从源码中很容易看出来,分别处理不同的 hook 点,然后中间有根据
aliasSelector 找到之前方法的实现,然后执行。

小结

初步的源码分析就是这个样子,没有太关注一些细节,也存在一些自己现在还不是很熟悉的处理方式,毕竟涉及到太多的 swizzling,消息转发一类的方法,这一块的只是需要后期在多研究 runtime 来提高。

代码中有一些其他的比较小的方法没有讲到,大家自己自行看一下。

参考

https://wereadteam.github.io/…

http://blog.ypli.xyz/ios/aop-…

https://blog.csdn.net/weixin_…

正文完
 0