Runtime窥探 (六)| AOP与Aspects核心源码

前言

如何把这个世界变得美好?把你自己变得更美好

秋天来了

我们这篇博客继续来介绍Runtime在开发中的实际应用,通过开源库Aspects来对runtime有更好的认识和理解。

一、Aspects库

这个库是iOS基于AOP编程思想的开源库,用于跟踪修改一个指定的类的某个方法执行前/替换/后,同时可以自定义添加一段代码块.对这个类的所有对象都会起作用。

所有的调用都会是线程安全的.Aspects 使用了Runtime的消息转发机制以及method swizzling,会有一定的性能消耗.所有对于过于频繁的调用,不建议使用 Aspects.Aspects更适用于视图/控制器相关的等。并不适用于每秒调用不超过1000次的代码.

二、AOP

基本概念:

  • AOP(Aspect Oriented Programming)是面向切面编程。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术的编程思想。

  • 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

主要功能:

  • 日志记录,性能统计,安全控制,事务处理,异常处理,事务的处理等等。

主要意图:

  • 将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

说明:

  • 我们在开发一个应用程序的时候,把系统分为很多模块(如:首页、分类、购物车、我的等模块)。我们从立体上看就是一个并列的树形结构,属于纵向切入系统。也就是OOP的的编程思想(面向对象编程思想),而AOP就属于横向切入系统,把整个系统的重复操的的部分提取出来(Log打印、日志记录、应用系统的异常捕捉及处理等等),由此可见,AOP是OOP的一个有效补充。

  • 假设把应用程序想成一个立体结构的话,OOP的利刃是纵向切入系统,把系统划分为很多个模块(如:用户模块,文章模块等等),而AOP的利刃是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等)。

注意:

  • AOP不是一种具体代码实现的技术,实际上是编程思想。凡是符合AOP思想的技术编程,都可以看成是AOP的实现。

三、解析Aspects库

我们经常用的Method Swizzling就是一种AOP思想实现,Aspects是比较很棒的基于AOP编程思想的开源库,
由于Aspects的代码较多,我们只是来阅读Aspects的核心实现思路和流程。

1.Aspects的基本模块

  • AspectInfo

    • Aspect信息类:用来保存信息的,存放被hook的实例、方法、参数等
  • AspectIdentifier

    • Aspect标识类:用来追踪一个唯一的aspect,AspectIdentifier对应的实例,里面会包含了单个的 Aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等。初始化AspectIdentifier的过程实质是把我们传入的block打包成AspectIdentifier。
  • AspectsContainer

    • 是一个用来存储所有的aspect的容器,可能存储实例方法/类方法。所以会有两种容器
  • AspectTracker

    • AspectTracker来跟踪要被hook的类

上面这些模块都是用来辅助核心思想实现的,使开源库模块清晰、较高容错率、职责明确等等,这些模块还是比较好理解的,就不一一阅读了。其实很多优秀开源库都会有类似的模块(比如信息、容器、唯一标识等等)。下面我们主要了解Aspects的核心思想以及流程。

2.小插曲

为什么大多数开源库都会有这些模块?举个例子:

和女朋友一起去溜一堆狗......

  • 我们怎么来控制一堆狗不乱跑呢?那就用绳子把狗拴起来,把绳子握在手里或者你想绑在腿上,这时候手或腿就是来控制所有狗的工具,也就是我们所说的容器(用来保存所有的个体信息)。

  • 我们怎么来看这个狗的名字、品种、年龄?我们可以制作一个标签挂在狗脖子上,上面写着狗的基本信息。也就是我们所说的信息模块(用来存储个体基本信息)。

  • 现在女朋友要溜xx这条狗,怎么给她?就需要一个唯一标识狗个体的工具,那我们可以在绳子上有个编号便签,也就是我们所说的唯一标识模块(标识某个个体,包含基本信息以及其他辅助信息)。这样我们可以通过编号就可以找到对应的狗,而且不会找错,这样就可以愉快的来遛狗。

  • 上面这些工作都是来辅助我们遛狗(核心模块)

3.Aspects对外接口以及基本说明

通过源代码Aspects中可以看到下面两个对外公开接口用于hook selector

@interface NSObject (Aspects)

/***********************
第一个参数selector:是要给它增加切面的原方法
第二个参数是AspectOptions:是代表这个切片增加在原方法的before / instead / after
第三个入参block:这个block复制了正在被hook的方法的签名signature类型
第一个参数selector将返回一个遵循<AspectInfo>的id对象,这个对象继承了方法的所有参数,
这些参数都会被填充到匹配的block的签名里
你也可以使用一个空block,或者一个简单的id<AspectInfo>
不支持hook静态static方法的
返回一个可以用来撤销aspect的token
***********************/

//hook类方法,hook一个类的所有实例对应的一个方法
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;
//hook实例方法,hook类的一个具体实例对应的一个方法
//为一个具体实例的seletor的执行 之前/或者被替换/之后 添加一个block代码
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

@end

方法说明:

  • 第一个方法为类方法:也就是说接受者是一个要被hook的类,也就是说hook一个类的所有实例对应的一个方法。会对类进行消息转发和method swizzling。会对类中methodLists的两个方法进行修改:

    • 1.forwardInvocation:
      forwardInvocation:的IMP(方法实现)被替换为:__ASPECTS_ARE_BEING_CALLED__,这个函数内部具体执行被hook的selector和切入操作的实现。forwardInvocation: 的本来的IMP被保存在__aspects_forwardInvocation:中。在调用aspect_hookClass()函数会进行forwardInvocation:的替换

    • 2.要被hook的selector:要被hook的selector的IMP被替换为:_objc_msgForward/_objc_msgForward_stret,这个函数用于直接触发消息转发机制,而不会在methodLists查找函数来执行。被hook的selector的原始IMP被保存在方法aspects_selector中。在调用aspect_prepareClassAndHookSelector()函数会进行selector的替换

    • 3.当我们调用[objc message];时就直接触发消息转发机制调用forwardInvocation:方法,而实现就是__ASPECTS_ARE_BEING_CALLED__这个函数(真正执行的函数实体),从而实现selector的hook。

  • 第二个方法为实例方法:也就是说接受者是一个要被hook的类的实例对象,也就是说hook类的一个具体实例对应的一个方法。这里跟上面区别主要是新建子类用来操作hook,具体步骤如下:
    • 1.新建一个要被hook的类的子类:xxx__Aspects_
    • 2.把要hook的实例的isa指向上面新生成的子类xxx__Aspects_,也就是说当前这个实例变成了子类xxx__Aspects_的实例对象。
    • 3.对上面新建子类xxx__Aspects_进行消息转发和method swizzling。具体思路就是和上面的类方法流程一样。

上面是Aspects的核心思想以及流程的简单说明,下面我们对这些核心代码进行梳理介绍。

注意:

  • Aspects并不能hook类的类方法
  • Aspects不能hook静态方法
  • Aspects不能hook类中不存在或者未实现的方法

4.Aspects核心代码解析

对外接口源码

1.上图说明:

  • 不管调用hook类的的实例方法还是类方法,在函数内都会调起私有c函数static id aspect_add()来进行统一处理
  • 使用自旋锁来保证线程安全执行
  • 然后会进行前期准备工作处理(如黑名单排除、生成标识实例以及添加、block块的函数签名和转换处理等等),这些通过源码还是比较好理解的
  • 把前期准备工作做完后,就会调用函数aspect_prepareClassAndHookSelector(self, selector, error);来进行核心模块实现

下面我们对函数aspect_prepareClassAndHookSelector(self, selector, error);来进行查看源码

2.核心函数aspect_prepareClassAndHookSelector()

函数aspect_prepareClassAndHookSelector()的具体实现如下:

aspect_prepareClassAndHookSelector
  • 函数主要分类两部分处理:
    • 一个是hook Class的处理,这一部分主要实现上面提到的forwardInvocation:函数的替换具体过程
    • 一个是hook selector的处理,这一部分主要实现上面提到的要把被hook的selector的函数实现替换成_objc_msgForward/_objc_msgForward_stret,直接触发消息转发机制调用forwardInvocation:

3.hook Class过程

我们先来看一下核心函数aspect_prepareClassAndHookSelector()中的第一部分hook Class过程,这个会调用aspect_hookClass函数

aspect_hookClass
  • 上面根据self获取类对象/元类的Class

  • 先对当前类进行筛选,如果当前Class是以xxx_Aspects_后缀结尾的名称,说明这个Class已经被hook过了,不需要再进行下面重复处理,直接返回当前Class,去执行hook selector的过程(后面会说)

  • 然后再看baseClass是不是元类,object_getClass(self)获取self的isa指针。如果当前是类对象,则class_isMetaClass(baseClass)是元类,说明当前hook的是某一个类的所有实例的对应方法。直接调用函数aspect_swizzleClassInPlace()(后面介绍)来method swizzling函数forwardInvocation:

  • 判断当前实例对象的isa指向statedClass和baseClass,按理说当self为实例变量时,object_getClass(self)与[self class]输出结果一直,均获得isa指针,即指向类对象的指针。但是这里判断相不相等?我们上一篇博客说过KVO过的对象的isa会指向一个中间类NSKVONotifying_XXX,所以说不相等时,说明这个实例对象是被KVO观察的对象。直接调用函数aspect_swizzleClassInPlace()来method swizzling函数forwardInvocation:

  • 上面情况都排除了,说明hook的是某一个类的实例的对应方法,下面就是hook类方法和实例方法的区别了

    • 1.新建当前self的所属类的子类:xxx__Aspects_
    • 2.调用aspect_swizzleForwardInvocation()替换函数forwardInvocation:的实现
    • 3.调用函数aspect_hookedGetClass(),把新建子类xxx__Aspects_的isa指向self的所属类,把新建子类xxx__Aspects_的元类的isa指向self的所属类
    • 4.上面完成后,注册新类说明新建子类创建完毕
    • 5.把当前self的isa指向新建子类xxx__Aspects_,成功的把self hook成了其子类 xxx_Aspects_,也就是self所属类是xxx_Aspects_,而不再是原始类xxx了。

上面就是整个hook Class的过程,流程图如下:

hook Class

上面都会调用有一个函数aspect_swizzleClassInPlace,这个函数的作用就是我们来替换Class系统方法forwardInvocation:的实现,源代码如下

//替换类的快速消息转发方法,并把类添加到交换类的集合中
static Class aspect_swizzleClassInPlace(Class klass) {
    NSCParameterAssert(klass);
    NSString *className = NSStringFromClass(klass);

    _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
        if (![swizzledClasses containsObject:className]) {
            //不包含,就调用aspect_swizzleForwardInvocation()方法,并把className加入到Set集合里面。
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    });
    return klass;
}

//类的forwardInvocation方法替换为__ASPECTS_ARE_BEING_CALLED__的实现,返回新函数imp
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
    NSCParameterAssert(klass);

    //替换类中已有方法的实现,返回原来函数imp
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    
    if (originalImplementation) {
        //originalImplementation不为空的话说明原方法有实现,添加一个新方法保存原来类的ForwardInvocation方法实现
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
  • 从上面代码中看出,把传入的Class加入到集合中用于hook完成后若需移除时用到,同时调用函数aspect_swizzleForwardInvocation()
  • 函数aspect_swizzleForwardInvocation()中可以看到:使用class_replaceMethod方法把Class的forwardInvocation:函数实现替换成了函数__ASPECTS_ARE_BEING_CALLED__,这个才是真正的函数执行入口。同时把类的原始forwardInvocation:函数实现保存在了__aspects_forwardInvocation:,用于后面hook selector不成功时,调用原始的forwardInvocation:函数来执行或者抛出异常等。

hook Class总结:到这里就把hook Class的过程解析完成了,说到底过程就是处理要被hook的类,同时把类的消息转发方法forwardInvocation:替换成__ASPECTS_ARE_BEING_CALLED__函数。

4.hook selector过程

在核心函数aspect_prepareClassAndHookSelector()中的hook Class处理在上面已经解析完成,现在我们来继续往下解析hook selector。这一部分源码如下:

Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
    //当前imp不是消息转发方法
    //获取当前原始的selector对应的IMP的方法编码typeEncoding
    const char *typeEncoding = method_getTypeEncoding(targetMethod);
    
    //给原始方法添加一个前缀名"aspects__XX"
    SEL aliasSelector = aspect_aliasForSelector(selector);
    
    if (![klass instancesRespondToSelector:aliasSelector]) {
        //没有找到新方法"aspects__XX",就添加一个新方法
        __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);
    }
    
    //我们使用消息转发forwardInvocation来进行hook
    //把当前的sel方法的替换成forwardInvocation方法,selector被执行的时候,直接会触发消息转发从而进入forwardInvocation
    class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
  • 1.获取要被hook的方法Method以及函数实现IMP指针

  • 2.判断当前方法实现是不是消息转发方法,如果是直接返回不作处理,hook不成功。不是继续往下执行3

  • 3.获取原始的selector对应的IMP的方法编码typeEncoding,以及给原始方法添加一个前缀名"aspects__XX",获取SEL,这个sel保存了原始方法的实现。这样才hook的过程中,如果不成功会调用这个sel,走原始代码,不会影响正常函数。

  • 4.如果没有找到这个方法就调用class_addMethod给这个Class添加一个新方法.

  • 5.调用class_replaceMethod函数来把selector的函数实现替换成forwardInvocation:,这样调用selector时就回走forwardInvocation:函数,而这个函数在hook Class的过程中也被替换成__ASPECTS_ARE_BEING_CALLED__函数,真正实现的函数入口就是这个。

hook selector总结:说到底这个过程就是处理要被hook的selector,把selector方法的实现替换成的消息转发方法forwardInvocation:

5.举例说明核心流程:

  • 新建类A
  • 给类A添加一个对象方法method以及实现
  • 初始化一个A类实例对象a

我们准备hook初始化阶段调用下面代码:

[a aspect_hookSelector:@selector(method) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo>aspectInfo,){
        NSLog(@"arguments = %@",aspectInfo.arguments);    
} error:NULL];
  • 1.因为hook的是实例方法,所以在hook Class的时候会新建子类:A__Aspects_(中间类)
  • 2.调用aspect_swizzleForwardInvocation()把A__Aspects_类的forwardInvocation:函数实现替换成__ASPECTS_ARE_BEING_CALLED__
  • 3.把A__Aspects_的isa指向A,也就是把A__Aspects_类和A类一模一样。
  • 4.把self,也就是a的所属类变成A__Aspects_类。
  • 5.给A__Aspects_类的添加一个和原始要被hook的方法一样的函数,用于保存原始方法的实现
  • 6.替换A__Aspects_类的要被hook的方法,替换成forwardInvocation:函数

我们在hook执行过程调用下面代码:

[a method];
  • 因为在hook初始化阶段时,把method替换成了forwardInvocation:函数。forwardInvocation:函数又被替换成了__ASPECTS_ARE_BEING_CALLED__函数。所以[a method]的函数实现就是__ASPECTS_ARE_BEING_CALLED__函数。
  • __ASPECTS_ARE_BEING_CALLED__函数就是我们hook切入的具体操作。如果我们hook没有成功时,也会调用原始的selector方法或者抛出异常,不会影响正常函数的实现。

5.Aspects总结

Aspects核心思想就是通过runtime的消息转发机制和method swizzling生成中间类来替换函数实现。这种思想和上一篇KVO的底层实现很相似。可以仔细阅读里面的代码,学习相关的实现思想以及优秀的代码片段。

我没有把所有的代码都一一解析,如果想看代码注释的,博客最后会有注释的项目地址。我对Aspects的库的中文注释以及理解说明,有兴趣的可以下载看下。

有注释的Aspects地址:https://git.coding.net/Dely/JYAOPDemo.git

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

推荐阅读更多精彩内容