Aspects深度解析-iOS面向切面编程

背景简述

在平常开发过程当中是否有过这样的需求:不修改原来的函数,可是又想在函数的执行先后插入一些代码。这个方式就是面向切面(AOP),在iOS开发中比较知名的框架就是Aspects,而饿了么新出的Stinger框架先不讨论,Aspects的源码精炼巧妙,很值得学习深究,本文主要从源码和应用层面来介绍下git

源码解析

先提出几个问题

带着问题去阅读更容易理解github

  1. Aspects实现的核心原理是什么
  2. 哪些方法不能被hook
  3. hook的操做是否能够只对某个实例生效,对同一个类的其余实例不生效?
  4. block是如何被存储和调用的

基本原理

正常来说想实现AOP,能够利用runtime的特性进行method swizzle,但Aspects就是造好的轮子,并且更好用,下面简述下Aspects的基本原理安全

runtime的消息转发机制

在OC中,全部的消息调用最后都会经过objc_msgSend()方法进行访问bash

  1. 经过objc_msgSend()进行消息调用,为了加快执行速度,这个方法在runtime源码中是用汇编实现的
  2. 而后调用lookUpImpOrForward()方法,返回值是个IMP指针,若是查找到了调用函数的IMP,则进行方法的访问
  3. 若是没有查到对于方法的IMP指针,则进行消息转发机制
  4. 第一层转发:会调用resolveInstanceMethod:、resolveClassMethod:,此次转发是方法级别的,开发者能够动态添加方法进行补救
  5. 第二层转发:若是第一层转发返回NO,则会进行第二层转发,调用forwardingTargetForSelector:,能够把调用转发到另外一个对象,这是类级别的转发,调用另外一个类的相同的方法
  6. 第三层转发:若是第二层转发返回nil,则会进入这一层处理,这层会调用methodSignatureForSelector:、forwardInvocation:,此次是完整的消息转发,由于你能够返回方法签名、动态指定调用方法的Target
  7. 若是转发都失败,就会crash

Aspects的基本原理

对外暴露的核心API框架

/**
做用域:针对全部对象生效
selector: 须要hook的方法
options:是个枚举,主要定义了切面的时机(调用前、替换、调用后)
block: 须要在selector先后插入执行的代码块
error: 错误信息
*/
+ (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;

复制代码

上面介绍了消息的转发机制,而Aspects就是利用了消息转发机制,经过hook第三层的转发方法forwardInvocation:,而后根据切面的时机来动态调用block。接下来详细分析巧妙的设计ide

  1. 类A的方法m被添加切面方法
  2. 建立一个类A的子类B,并hook子类B的forwardInvocation:方法拦截消息转发,使forwardInvocation:IMP指向事先准备好的__ASPECTS_ARE_BEING_CALLED__函数(后面简称ABC函数),block方法的执行就在ABC函数中
  3. 把类A的对象的isa指针指向B,这样就把消息的处理转发到类B上,相似KVO的机制,同时会更改class方法的IMP,把它指向类A的class方法,当外界调用class时获取的仍是类A,并不知道中间类B的存在
  4. 对于方法m,类B会直接把方法m的IMP指向_objc_msgForward()方法,这样当调用方法m时就会走消息转发流程,触发ABC函数

详细分析

执行入口

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

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    __block AspectIdentifier *identifier = nil;
    // 添加自旋锁,block内容的执行时互斥的
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 获取容器,容器的对象以关联对象的方式添加到了当前对象上,key值为`前缀+selector`
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // 建立标识符,用来存储SEL、block、切面时机(调用前、调用后)等信息
            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_add(self, selector, options, block, error)方法,这个方法时线程安全的,接下来一步步解析具体作了什么函数

过滤拦截:aspect_isSelectorAllowedAndTrack()

精简版的源码,已经添加了注释学习

static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
    static NSSet *disallowedSelectorList;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{ // 初始化黑名单列表,有些方法时禁止hook的
        disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
    });

    // 第一步:检查是否在黑名单内
    NSString *selectorName = NSStringFromSelector(selector);
    if ([disallowedSelectorList containsObject:selectorName]) {
        ...
        return NO;
    }

    // 第二步: dealloc方法只能在调用前插入
    AspectOptions position = options&AspectPositionFilter;
    if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
        ...
        return NO;
    }
    // 第三步:检查类是否存在这个方法
    if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
        ...
        return NO;
    }

    // 第四步:若是是类而非实例(这个是类,不是类方法,是指hook的做用域对全部对象都生效),则在整个类即继承链中,同一个方法只能被hook一次,即对于全部实例对象都生效的操做,整个继承链中只能被hook一次
    if (class_isMetaClass(object_getClass(self))) {
        ...
    } else {
        return YES;
    }
    return YES;
}

复制代码
  1. 不容许hookretainreleaseautoreleaseforwardInvocation:,这些很少解释
  2. 容许hookdealloc,可是只能在dealloc执行前,这都是为了程序的安全性设置的
  3. 检查这个方法是否存在,不存在则不能hook
  4. Aspects对于hook的生效做用域作了区分:全部实例对象&某个具体实例对象。对于全部实例对象在整个继承链中,同一个方法只能被hook一次,这么作的目的是为了规避循环调用的问题(详情能够了解下supper关键字)

关键类结构

AspectOptions

是个枚举,用来定义切面的时机,即原有方法调用前、调用后、替换原有方法、只执行一次(调用完就删除切面逻辑)ui

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// 原有方法调用前执行 (default)
    AspectPositionInstead = 1,            /// 替换原有方法
    AspectPositionBefore  = 2,            /// 原有方法调用后执行
    
    AspectOptionAutomaticRemoval = 1 << 3 /// 执行完以后就恢复切面操做,即撤销hook
};

复制代码

AspectIdentifier类

简单理解话就是一个存储model,主要用来存储hook方法的相关信息,如原有方法、切面block、切面时机等atom

@interface AspectIdentifier : NSObject
...其余省略
@property (nonatomic, assign) SEL selector; // 原来方法的SEL
@property (nonatomic, strong) id block; // 保存要执行的切面block,即原方法执行先后要调用的方法
@property (nonatomic, strong) NSMethodSignature *blockSignature; // block的方法签名
@property (nonatomic, weak) id object; // target,即保存当前对象
@property (nonatomic, assign) AspectOptions options; // 是个枚举,表示切面执行时机,上面已经有介绍
@end

复制代码

AspectsContainer类

容器类,以关联对象的形式存储在当前类或对象中,主要用来存储当前类或对象全部的切面信息

@interface AspectsContainer : NSObject
...其余省略
@property (atomic, copy) NSArray <AspectIdentifier *>*beforeAspects; // 存储原方法调用前要执行的操做
@property (atomic, copy) NSArray <AspectIdentifier *>*insteadAspects;// 存储替换原方法的操做
@property (atomic, copy) NSArray <AspectIdentifier *>*afterAspects;// 存储原方法调用后要执行的操做
@end

复制代码

存储切面信息

存储切面信息主要用到了上面介绍的AspectsContainerAspectIdentifier这两个类,主要操做以下(注释写的已经很详细)

  1. 获取当前类的容器对象aspectContainer,若是没有则建立一个
  2. 建立一个标识符对象identifier,用来存储原方法信息、block、切面时机等信息
  3. 把标识符对象identifier添加到容器中
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    ...
    // 获取容器对象,主要用来存储当前类或对象全部的切面信息,容器的对象以关联对象的方式添加到了当前对象上,key值为`前缀+selector`
    AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
    // 建立标识符,用来存储SEL、block、切面时机(调用前、调用后)等信息
    identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
    if (identifier) {
        // 把identifier添加到容器中
        [aspectContainer addAspect:identifier withOptions:options];
        ...
    }
    return identifier;
}
复制代码

建立中间类

这一步的操做相似kvo的机制,隐式的建立一个中间类,一:能够作到hook只对单一对象有效,二:避免了对原有类的侵入

这一步主要作了几个操做

  1. 若是已经存在中间类,则直接返回
  2. 若是是类对象,则不用建立中间类,并把这个类存储在swizzledClasses集合中,标记这个类已经被hook了
  3. 若是存在kvo的状况,那么系统已经帮咱们建立好了中间类,那就直接使用
  4. 对于不存在kvo且是实例对象的,则单首创建一个继承当前类的中间类midcls,并hook它的forwardInvocation:方法,并把当前对象的isa指针指向midcls,这样就作到了hook操做只针对当前对象有效,由于其余对象的isa指针指向的仍是原有类
static Class aspect_hookClass(NSObject *self, NSError **error) {
	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);
        }else if (statedClass != baseClass) {
        // Probably a KVO class. Swizzle in place. Also swizzle meta classes in place.
        return aspect_swizzleClassInPlace(baseClass);
        }

    // Default case. Create dynamic subclass.
	const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
	Class subclass = objc_getClass(subclassName);

	if (subclass == nil) {
	    subclass = objc_allocateClassPair(baseClass, subclassName, 0);
            // hook forwardInvocation方法
	    aspect_swizzleForwardInvocation(subclass);
            // hook class方法,把子类的class方法的IMP指向父类,这样外界并不知道内部建立了子类
	    aspect_hookedGetClass(subclass, statedClass);
	    aspect_hookedGetClass(object_getClass(subclass), statedClass);
	    objc_registerClassPair(subclass);
	}
    // 把当前对象的isa指向子类,相似kvo的用法
	object_setClass(self, subclass);
	return subclass;
}
复制代码

替换forwardInvocation:方法

从下面的代码能够看到,主要功能就是把当前类的forwardInvocation:替换成__ASPECTS_ARE_BEING_CALLED__,这样当触发消息转发的时候,就会调用__ASPECTS_ARE_BEING_CALLED__方法

对于__ASPECTS_ARE_BEING_CALLED__方法是Aspects的核心操做,主要就是作消息的调用和分发,控制方法的调用的时机,下面会详细介绍

// hook forwardInvocation方法,用来拦截消息的发送
static void aspect_swizzleForwardInvocation(Class 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));
}
复制代码

自动触发消息转发机制

Aspects的核心原理是消息转发,那么必要出的就是怎么自动触发消息转发机制

runtime中有个方法_objc_msgForward,直接调用能够触发消息转发机制,著名的JSPatch框架也是利用了这个机制

假如要hook的方法叫m1,那么把m1IMP指向_objc_msgForward,这样当调用方法m1时就自动触发消息转发机制了,详细实现以下

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)) {
        ...
        // We use forwardInvocation to hook in. 把函数的调用直接触发转发函数,转发函数已经被hook,因此在转发函数时进行block的调用
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    }
}
复制代码

核心转发函数处理

上面一切准备就绪,那么怎么触发以前添加的切面block呢,首先咱们梳理下整个流程

  1. 方法m1IMP指向了_objc_msgForward,调用m1则会自动触发消息转发机制
  2. 替换forwardInvocation:,把它的IMP指向__ASPECTS_ARE_BEING_CALLED__方法,消息转发时触发的就是__ASPECTS_ARE_BEING_CALLED__

上面操做能够直接看出调用方法m1则会直接触发__ASPECTS_ARE_BEING_CALLED__方法,而__ASPECTS_ARE_BEING_CALLED__方法就是处理切面block用和原有函数的调用时机,详细看下面实现步骤

  1. 根据调用的selector,获取容器对象AspectsContainer,这里面存储了这个类或对象的全部切面信息
  2. AspectInfo会存储当前的参数信息,用于传递
  3. 首先触发函数调用前的block,存储在容器的beforeAspects对象中
  4. 接下来若是存在替换原有函数的block,即insteadAspects不为空,则触发它,若是不存在则调用原来的函数
  5. 触发函数调用后的block,存在在容器的afterAspects对象中
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation: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];
                break;
            }
        }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    }

    // After hooks. 方法执行以后调用
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);

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

复制代码

总结

Aspects的核心原理是利用了消息转发机制,经过替换消息转发方法来实现切面的分发调用,这个思想很巧妙并且应用很普遍,不少三方库都利用了这个原理,值得学习

目前这个库已经很长时间没有维护了,原子操做的支持使用的仍是自旋锁,目前这种锁已经不安全了

另外使用这个库是须要注意相似原理的其余框架,可能会有冲突,如JSPatch,不过JSPatch已经被封杀了,但相似需求有不少

相关文章
相关标签/搜索