框架解析及底层iOS

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

2021-01-15  本文已影响0人  L_Ares

本文为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库(二)进行探索。

上一篇下一篇

猜你喜欢

热点阅读