美文网首页框架解析及底层iOS
第三十七节—AOP之Aspects库(一)

第三十七节—AOP之Aspects库(一)

作者: L_Ares | 来源:发表于2021-01-15 17:49 被阅读0次

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。

    一、切面编程AOP

    AOP,其实这种思想在之前的Method_Swizzling用到过。

    在实际开发中,你是否出现过一种开发需求 :

    需求 :

    1. 不修改原有的方法。
    2. 但想要在原有的函数实现的最开始位置最末尾位置添加一些代码,又不改变原有函数的其他代码。
    3. 或者直接想在某个地方替换掉原有函数的实现。
    4. 又或者做埋点。

    这样的情况下,就需要用到切面编程思想,也是我们常说的hook

    问题1 : 什么是AOP面向切面

    • AOP :

      • 英文全称 : Aspect Oriented Programming
      • 中文全称 : 面向切面的程序设计、剖面导向程序设计。
      • AOP是计算机科学中的一种程序设计思想,只是一种编程范式,并不是具体的某种实现。
    • 切入点 : 一般情况下,在iOS中,要被hook的类或者要被hook的方法可以叫做切入点。

    • 切面 : 一般情况下,在iOS中,加入到切入点的代码片段可以简单的理解为切面。

    • 切面编程思想 : 一般情况下,在运行时,动态的将代码切入到类的指定方法、指定位置上的思想可以叫做AOP面向切面思想。

    • 实现方式 : iOS中,多数是通过代理机制实现。

    (1). AOP可以理解为对OOP(面向对象思想)的一种补充,它可以解决OOP中不同类中的相同代码造成的代码冗余,却不会造成高耦合。
    (2). OOPAOP组成了一个坐标轴,OOPX轴AOPY轴
    (3). OOP在横向的分出了很多的类进行封装,很好的解决了职责分配的问题。
    (4). AOP在纵向上向特定的代码中动态的加入需要的特定代码。

    问题2 : 切面编程的常见应用场景

    1. 参数校验 : 例如网络请求发送前的参数校验,网络请求返回数据的格式校验。

    2. 无痕埋点 : 统一处理埋点,降低代码的耦合度。

    3. 页面统计 : 帮助统计页面的访问量。

    4. 事务处理 : 拦截指定事件,添加触发事件。

    5. 异常处理 : 当代码出现异常报错时,可以使用面向切面的方式提前做处理,防止crash。

    6. 热修复 : AOP可以让我们的代码在被执行前被替换为另一段代码,可以根据这个思路,修复Bug。

    7. 代码分层 : 业务逻辑层和非业务逻辑层的方法进行分层,既不影响业务逻辑层,也可以使代码更有层次性。

    二、Aspects库的探索

    1. 举例方法进行探索

    为了方便对Aspects库的思路理解,就通过举例的方式进行探索。

    注意 : 因为本章节内容并不少,会把我所有的想法和解析全部都写上来,所以这个例子可能在后面的Aspects库(二)、甚至可能出现的Aspects库(三)中全部使用本节的例子,后面文章不再重复举例。

    操作

    1.1 创建一个iOS的Project,并且利用cocoapods导入Aspects

    图2.1.0

    1.2 打开xcworkspace文件,并且在ViewController.h文件中导入<Aspects/Aspects.h>

    图2.1.1

    1.3 在ViewController.h中声明方法- (void)testName:(NSString *)str Age:(int)age Sex:(NSString *)sex;并且在ViewController.m中实现如下图

    图2.1.2

    1.4 在viewDidLoad中利用Aspects库的公开API对上图中的方法进行hook,并调用它

    图2.1.3

    1.5 观察上述操作之后的举例运行结果

    图2.1.4

    1.6 从观察结果可以得到结论如下

    1. Aspects库的公开API入口是aspect_hookSelector: WithOptions: usingBlock: error方法。
    2. AspectPositionBefore也就是options参数控制了block参数所持有的函数的执行位置。
    3. 被Aspects库进行hook的方法,它的SEL发生了改变。

    1.7 探索思路

    1. 首先,从Aspects库的公开API入口进入Aspects库。
    2. 要知道options都有什么可以选择的参数,并且知道block参数中的函数是怎么被执行的。
    3. 为什么被hook的方法的SEL发生了改变,这是利用了什么思想和方法。

    2. Aspects库的入口之Aspects.h文件

    经过上面探索,我们可以知道,我们是利用了aspect_hookSelector: withOptions: usingBlock: error:方法对想要被hook的方法进行的改变。

    所以,就从这个方法来进入Aspects库的探索。

    操作

    进入Aspects库的Aspects.h文件,查看其公开的方法和类、属性、方法等。

    2.1 AspectOptions

    这是公开API中的options参数的可选值。是一个枚举类型,一共有4个枚举值可选择。详细情况见下图2.2.0的注释。

    图2.2.0

    2.2 AspectToken

    遵循NSObject协议,是Aspects库的一个令牌,可以用来注销一个aspect对象,也就是可以注销一个hook。详细情况见下图2.2.1的注释。

    图2.2.1

    2.3 AspectInfo

    这是Aspect库的协议。详细情况见下图2.2.2的注释。

    图2.2.2

    2.4 Aspects

    这是Aspects对象。

    1. 它利用了Runtime的消息转发机制进行hook。
    2. Aspects对象用来hook的会使系统产生一些性能上的消耗,所以不要使用它对频繁被调用的方法进行hook。
    3. Aspects主要针对的对象是view和controller中的方法。
    4. Aspects对象可以利用hook后返回的AspectToken进行注销。
    5. Aspects对象是线程安全的。
    6. AspectsNSObject的分类,之所以选择做NSObject的分类,是因为OC中大多数的类都是NSObject类的子类。
    7. Aspects不支持hook静态方法,也就是我们常说的类方法。
    图2.2.3

    2.5 AspectErrorCode

    这是Aspects库的公开API发生错误的时候,返回的错误代码。详情见下图2.2.4注释。

    图2.2.4

    3. Aspects库的入口之Aspects.m文件

    操作

    commond+鼠标左键点击我们在举例的时候使用的Aspects库的公开API,选择进入它的实现。

    图2.3.0

    3.1 公开API(id<AspectToken>)aspect_hookSelector: withOptions: usingBlock: error:

    可以看到,两个公开的API是名称是一模一样的,在下面的实现中也可以知道,两个API的实现也是一摸一样的。Aspects库的作者虽然不知道是不是苹果的官方人员,但是他的这种中间层设置模式和苹果官方的思想如出一辙。

    参数

    1. selector : 被hook的方法,也就是上面举例中的testName:Age:Sex:
    2. options : 枚举值。确定block参数中的函数在selector参数所代表的方法中的执行位置。可选值 :
      • AspectPositionAfter : 在原始方法实现后,调用block参数中的函数,这是Aspects库默认的options
      • AspectPositionInstead : 用block参数中的函数替换被hook的方法的原有实现。
      • AspectPositionBefore : 在原始方法实现前,调用block参数中的函数
      • AspectOptionAutomaticRemoval : 在第一次执行后,移除掉hook。也就是说,用这个参数的话,被hook的方法只能被hook一次。
    3. block : 用来插入或者替换selector参数所指向的方法的代码。它的第一个参数一定是id<AspectInfo>,可以选择写与不写,第二个参数开始就是被hook的方法的参数,也就是举例中的nameagesex。如果选择写上这些参数,则这些参数会被加入到block的签名信息中。你也可以不写block的参数或者只使用id<AspectInfo>参数。
    4. error : 该方法如果发生错误的话,这里会回调错误信息。

    方法实现

    图2.3.1

    可以看到,无论是实例方法还是类方法都是调用了aspect_add()函数,所以Aspects库的公开API实现的本质就是aspect_add()函数的实现。这里利用了苹果的一种代码结构的设计思想,就是中间层模式,全部利用中间层来进行实现,体现了低耦合性。

    3.2 aspect_add()函数

    这个函数就是Aspects库的公开API的实现本质。

    注意 : 因为Aspects库的公开API只有两个,又全部采用了中间层的方法设计模式,所以,aspect_add()函数要被分成几个部分来写,本节重点记录的是Aspects库对被hook的方法和被hook的类的校验逻辑。

    参数

    1. self : 调用API的类,也就是被hook的类,在举例中就是ViewController
    2. selector : 被hook的方法,在举例中就是testName:Age:Sex:
    3. options : block的执行位置,枚举值,上面的3.1中说过了,不再赘述。
    4. block : hook后要执行或者替换的代码块。
    5. error : aspect_add()函数的错误信息。

    函数实现

    3.2.1 整体逻辑
    图2.3.2 全图

    通过图2.3.2的源码可以看出,aspect_add()函数的实现本质的核心都在被收缩起来的block函数块里面。

    3.2.2 aspect_performLocked

    这是一把OSSpinLock的锁对象,之前在锁的章节说过,它是自旋锁,并且现在它的底层实现是基于os_unfair_lock的。

    图2.3.3
    3.2.3 aspect_isSelectorAllowedAndTrack

    这是验证利用Aspects库进行hook的方法(selector)是否符合Aspects库的标准,以及对元类对象(也就是类)做改变。

    步骤1 : 建立黑名单
    图2.3.4
    步骤2 : 检查黑名单
    图2.3.5
    步骤3 : 额外的检查
    图2.3.6

    这里我们可以得到两个要素 :

    1. 如果要hook的方法是dealloc方法,那么,position参数只能选择AspectPositionBefore枚举值。
    2. 如果要hook的方法并没有被要hook的类声明且实现,是不可以被hook的。
    步骤4 : 元类对象的操作

    (1). 首先,判断被hook的类是不是元类对象(类)

    图2.3.7

    看图2.3.7,if判断中使用的是object_getClass,这就和下面的[self class]形成了对比。

    1. object_getClass(self) : 获取selfisa指针指向。
    2. [self class] :
      • 如果self是实例对象,也就是由类初始化构造出来的对象,那么结果是类。
      • 而如果self是类对象,也就是由元类初始化构造出来的对象,那么结果是类本身。

    (2). 如果进入了if,那么代表被hook的对象就是元类对象。

    (3). 如果没有进入if,则返回YES,证明通过了Aspects对被hook对象和方法的验证。

    (4). if中,对元类操作的逻辑。

    首先,利用[self class]获取类。
    这里一定要看上面[self class]object_getClass(self)的区别,因为这里无论self是实例对象还是类,获得的klasscurrentClass都是普通的类,而不是元类。
    swizzledClassesDict是一个静态的、通过单例创建的字典,保证了唯一性。

    图2.3.8

    其次,两个do{...}while()循环,循环条件都是追溯currentClass的继承链,也就是被hook的对象所属的类的继承链。
    所以,对元类对象(类)的hook的可执行条件,就是两个do{...}while()循环中的逻辑。

    (5). 先看第二个do{...}while()循环。

    想要进入到第二个do{...}while()循环,则必定不会在第一个do{...}while()循环的时候出现return的现象。之所以先看第二个循环,是因为我们需要知道AspectTracker到底是作为一个怎样的类。

    图2.3.9

    这里我们可以知道 :

    1. 父类的tracker对象中,parentEntry属性是子类的tracker对象。
    2. 对于元类对象来说,只要子类被hook,其继承链上的所有父类的trackerselectorNames集合都会存储被hook的方法名称。
    3. AspectTracker是一个追踪器。
      • trackerClass表示被追踪的类。
      • selectorNames存储被hook的类中被hook的方法的名称,被hook的类的父类的trackerselectorNames集合也会存储被hook的子类的被hook的方法的名称。
      • parentEntry存放下一级别子类的追踪器。
    图2.3.10 图2.3.11

    (6). 再看第一个do{...}while()循环。

    先看第一个do{...}while()的整体结构。

    图2.3.12

    再看其中最重点的if结构。

    图2.3.13

    图2.3.13中可以得出 :

    每个类的层次结构或者说继承链中,相同的方法只能被该层次中的类hook一次,不能同时多次hook同一方法。

    注释

    校验之后的核心内容,将会放入下一节,AOP之Aspects库(二)进行探索。

    相关文章

      网友评论

        本文标题:第三十七节—AOP之Aspects库(一)

        本文链接:https://www.haomeiwen.com/subject/gantnktx.html