iOS - 切面编程 (Aspects解析)

前言

首先我们了解几个概念,什么是切面编程?切面编程的实际应用?

切面编程(AOP):什么是切面?举个栗子:切一根萝卜,切成三段;那这根萝卜就好比是一个功能模块,一段一段的萝卜好比是不同类实例化的对象,一根切成仨段的时候就是OOP(以VC为主组成);要是多个萝卜一起切就是AOP(以Modle组成项目模块)一刀好几段,切面的萝卜就像相同类实例化的对象;

为什么要用到切面编程呢? 比如我们有一个需求,需要在不修改原函数情况下,在函数执行前添加一些方法;这就需要用到切面编程思想;饿了么有出一款类似新框架,Stinger暂不讨论,本章介绍一下Aspect,是切面编程的实际应用,详细介绍从源码和应用层。

Aspects原理解析以及实践

Aspects概念

Aspects源码地址:Aspects源码

Aspects就是利用runtime特性进行method swizzle,hook住对应的方法进行添加或修改等操作;一个封装好的轮子。

Aspects原理

1.基本原理

知道了Aspects 是利用runtime的特性,我们来了解一下基本的runtime消息转发机制;

1.首先由objc_msgSend()发起消息的汇编查找流程;

2.然后会调用lookUpImpOrForward() 来查找IMP指针,如果找到了则调用函数的IMP,进行方法访问;否则没找到IMP方法,下一步则进入消息转发机制。

3.第一层转发:由receiveInstanceMethod()receiveClassMethod方法进行接收,这层是方法层面的,可以通过开发者进行动态方法添加进行补救;

4.第二层转发:如果上层没找到SEL,则进入第二层转发,调用forwardingTargetForSelector(),这层是类级别转发,可以把调用转到另一个类对象,调用另一个类的相同方法。

5.第三层转发:如果第二次没找到,返回nil;则进入最后一次转发,这层会调用methodSignatureForSelector(),forwardInvocation();这层是动态方法签名,和动态指定调用方法的target,可以从这层进行消息方法的替换添加等操作;

6.若上述都没找到SEL,则会cash抛出异常;

基本原理分析:

从对外暴露的核心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就是利用了转发机制的第三层forwardInvocation(),Hook住其方法,然后利用切面编程思维来动态调用block。下面来细分其设计:

(1)A类的方法.m被添加切面方法;

(2)新建一个B类继承于A类,并且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)对于A类方法m,B类会直接把方法m的IMP指向_objc_msgForward()方法,这样当调用方法m时就会走消息转发流程,触发ABC函数

2.详细解析

我们先从实例类入口

- (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关键字)

(二)存储切面信息

一些切面信息主要存储在AspectIdentifier,AspectsContainer这两类中,关键操作如下(注解已详尽):

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

(三)创建中间类

这一步为关键中的一步,创建隐性中间类对原有类进行方法hook。酷似KVO的机制,其特点为:1.可以做到只对当一对象进行hook,2.避免原有类的入侵;

这一步骤中的关键操作为:

(1) 检查是否已创建中间类,有则返回,无则新建;

(2) 如果对象是类对象,则不需要创建中间类,并把这个类存储在swizzleClasses集合中,同时标记这个类为已被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;
}

下面来看看如何替换拦截:

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

从上面源码可以看出,其主要功能就是把forwardInvocation()替换成ASPECTS_ARE_BEING_CALLED,对于__ASPECTS_ARE_BEING_CALLED__方法是Aspects的核心操作,主要就是做消息的调用和分发,控制方法的调用的时机,下面会详细介绍。

(四)关键类结构

AspectOptions 枚举

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
  AspectPositionAfter   = 0,            /// 原有方法调用前执行 (default)
  AspectPositionInstead = 1,            /// 替换原有方法
  AspectPositionBefore  = 2,            /// 原有方法调用后执行

  AspectOptionAutomaticRemoval = 1 << 3 /// 执行完之后就恢复切面操作,即撤销hook
};

通过源码注解可以看出,这个枚举是用来定义切面的时机;即有调用前,替换时,执行后,只执行一次(调用完一次就删除切面逻辑)

AspectIdentifier 类

@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

从这个类来看,简单理解成一个Model,主要用来存储hook方法的相关信息。如切面方法,切面bolck,切面时机等;

AspectsContainer类

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

这是一个容器类,以关联对象的形式储存储存在当前对象或者类对象中,主要储存当前对象或者类对象的切面信息。

(五)自动触发消息转发机制

Aspect的核心原理就是消息转发机制,那有必要说一下如何自动触发自动转发机制的;

runtime中有个方法是_objc_msgForward,直接调用可以触发消息转发机制,JSPatch也是利用这个机制;

如果我们需要hook住方法t1,那么把t1IMP指针直接指向_objc_msgForward,那么t1方法调用直接触发消息自动转发机制了,实现如下:

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

(六)核心转发函数处理

从上面处理OK后,接下来我们如何触发切面的Block,下面我们来梳理一下:

(1)方法t1IMP指向_objc_msgForward,调用t1方法则会触发自动消息转发机制;

(2)第二部是替换forwardInvocation(),替换成__ASPECTS_ARE_BEING_CALLED__方法,消息自动转发的时候调用的其实就是__ASPECTS_ARE_BEING_CALLED__;

从上面分析可以看出,直接调用t1方法触发消息自动转发机制,其实就是触发了__ASPECTS_ARE_BEING_CALLED__方法;而__ASPECTS_ARE_BEING_CALLED__就是处理切面处理原有Block和触发时机的方法;下面我们来详细分析它的方法:

  1. 根据调用的selector,获取容器对象AspectContainer,里面储存了这个类和对象的所有切面信息;
  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已经被封杀了,但类似需求有很多

参考文章:Aspects深度解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345