第三十七节—AOP之Aspects库(一)
本文为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).OOP
和AOP
组成了一个坐标轴,OOP
是X轴
,AOP
是Y轴
。
(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
库
![](https://img.haomeiwen.com/i16702189/6618f1d91408650a.jpg)
1.2 打开xcworkspace
文件,并且在ViewController.h
文件中导入<Aspects/Aspects.h>
![](https://img.haomeiwen.com/i16702189/3b607b27cf282410.jpg)
1.3 在ViewController.h
中声明方法- (void)testName:(NSString *)str Age:(int)age Sex:(NSString *)sex;
并且在ViewController.m
中实现如下图
![](https://img.haomeiwen.com/i16702189/607b9c8367f06295.jpg)
1.4 在viewDidLoad
中利用Aspects
库的公开API
对上图中的方法进行hook,并调用它
![](https://img.haomeiwen.com/i16702189/e4d5bd789f9eabcc.jpg)
1.5 观察上述操作之后的举例运行结果
![](https://img.haomeiwen.com/i16702189/b9713e3550b8b92a.jpg)
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
的注释。
![](https://img.haomeiwen.com/i16702189/1053501ced2c98c7.jpg)
2.2 AspectToken
遵循NSObject协议,是
Aspects
库的一个令牌,可以用来注销一个aspect
对象,也就是可以注销一个hook。详细情况见下图2.2.1
的注释。
![](https://img.haomeiwen.com/i16702189/21e62186309f1f9f.jpg)
2.3 AspectInfo
这是
Aspect
库的协议。详细情况见下图2.2.2
的注释。
![](https://img.haomeiwen.com/i16702189/b021d174b649d8f7.jpg)
2.4 Aspects
这是
Aspects
对象。
- 它利用了
Runtime
的消息转发机制进行hook。Aspects
对象用来hook的会使系统产生一些性能上的消耗,所以不要使用它对频繁被调用的方法进行hook。Aspects
主要针对的对象是view和controller中的方法。Aspects
对象可以利用hook后返回的AspectToken
进行注销。Aspects
对象是线程安全的。Aspects
是NSObject
的分类,之所以选择做NSObject
的分类,是因为OC中大多数的类都是NSObject
类的子类。Aspects
不支持hook静态方法,也就是我们常说的类方法。
![](https://img.haomeiwen.com/i16702189/b4c7ff75ffe44909.jpg)
2.5 AspectErrorCode
这是
Aspects
库的公开API发生错误的时候,返回的错误代码。详情见下图2.2.4
注释。
![](https://img.haomeiwen.com/i16702189/a3953022dc1acd12.jpg)
3. Aspects库的入口之Aspects.m文件
操作
commond+鼠标左键
点击我们在举例的时候使用的Aspects
库的公开API,选择进入它的实现。
![](https://img.haomeiwen.com/i16702189/e3aea11647e6fee0.jpg)
3.1 公开API(id<AspectToken>)aspect_hookSelector: withOptions: usingBlock: error:
可以看到,两个公开的API是名称是一模一样的,在下面的实现中也可以知道,两个API的实现也是一摸一样的。
Aspects
库的作者虽然不知道是不是苹果的官方人员,但是他的这种中间层设置模式和苹果官方的思想如出一辙。
参数
selector
: 被hook的方法,也就是上面举例中的testName:Age:Sex:
。options
: 枚举值。确定block
参数中的函数在selector
参数所代表的方法中的执行位置。可选值 :
AspectPositionAfter
: 在原始方法实现后,调用block
参数中的函数,这是Aspects
库默认的options
。AspectPositionInstead
: 用block参数中的函数替换被hook的方法的原有实现。AspectPositionBefore
: 在原始方法实现前,调用block
参数中的函数AspectOptionAutomaticRemoval
: 在第一次执行后,移除掉hook。也就是说,用这个参数的话,被hook的方法只能被hook一次。block
: 用来插入或者替换selector
参数所指向的方法的代码。它的第一个参数一定是id<AspectInfo>
,可以选择写与不写,第二个参数开始就是被hook的方法的参数,也就是举例中的name
、age
、sex
。如果选择写上这些参数,则这些参数会被加入到block
的签名信息中。你也可以不写block
的参数或者只使用id<AspectInfo>
参数。error
: 该方法如果发生错误的话,这里会回调错误信息。
方法实现
![](https://img.haomeiwen.com/i16702189/000a8584a96ab051.jpg)
可以看到,无论是实例方法还是类方法都是调用了
aspect_add()
函数,所以Aspects
库的公开API实现的本质就是aspect_add()
函数的实现。这里利用了苹果的一种代码结构的设计思想,就是中间层模式,全部利用中间层来进行实现,体现了低耦合性。
3.2 aspect_add()函数
这个函数就是
Aspects
库的公开API的实现本质。
注意 : 因为
Aspects
库的公开API只有两个,又全部采用了中间层的方法设计模式,所以,aspect_add()
函数要被分成几个部分来写,本节重点记录的是Aspects
库对被hook的方法和被hook的类的校验逻辑。
参数
self
: 调用API的类,也就是被hook的类,在举例中就是ViewController
。selector
: 被hook的方法,在举例中就是testName:Age:Sex:
。options
:block
的执行位置,枚举值,上面的3.1中说过了,不再赘述。block
: hook后要执行或者替换的代码块。error
:aspect_add()
函数的错误信息。
函数实现
3.2.1 整体逻辑
![](https://img.haomeiwen.com/i16702189/99bce9c4f47d1dd4.jpg)
![](https://img.haomeiwen.com/i16702189/d122a133f6b30da9.png)
通过
图2.3.2
的源码可以看出,aspect_add()
函数的实现本质的核心都在被收缩起来的block
函数块里面。
3.2.2 aspect_performLocked
这是一把
OSSpinLock
的锁对象,之前在锁的章节说过,它是自旋锁,并且现在它的底层实现是基于os_unfair_lock
的。
![](https://img.haomeiwen.com/i16702189/0d1ed77f22b35d07.jpg)
3.2.3 aspect_isSelectorAllowedAndTrack
这是验证利用
Aspects
库进行hook的方法(selector
)是否符合Aspects
库的标准,以及对元类对象(也就是类)做改变。
步骤1 : 建立黑名单
![](https://img.haomeiwen.com/i16702189/403752db3f5f1c44.jpg)
步骤2 : 检查黑名单
![](https://img.haomeiwen.com/i16702189/415d9232bf59f2c6.jpg)
步骤3 : 额外的检查
![](https://img.haomeiwen.com/i16702189/88f2459d45a422a9.jpg)
这里我们可以得到两个要素 :
- 如果要hook的方法是
dealloc
方法,那么,position
参数只能选择AspectPositionBefore
枚举值。- 如果要hook的方法并没有被要hook的类声明且实现,是不可以被hook的。
步骤4 : 元类对象的操作
(1). 首先,判断被hook的类是不是元类对象(类)
![](https://img.haomeiwen.com/i16702189/a09d00deb0fedd88.png)
看图2.3.7,
if
判断中使用的是object_getClass
,这就和下面的[self class]
形成了对比。
object_getClass(self)
: 获取self
的isa
指针指向。[self class]
:
- 如果
self
是实例对象,也就是由类初始化构造出来的对象,那么结果是类。- 而如果
self
是类对象,也就是由元类初始化构造出来的对象,那么结果是类本身。
(2). 如果进入了if
,那么代表被hook的对象就是元类对象。
(3). 如果没有进入if
,则返回YES
,证明通过了Aspects
对被hook对象和方法的验证。
(4). if
中,对元类操作的逻辑。
首先,利用
[self class]
获取类。
这里一定要看上面[self class]
和object_getClass(self)
的区别,因为这里无论self
是实例对象还是类,获得的klass
和currentClass
都是普通的类,而不是元类。
而swizzledClassesDict
是一个静态的、通过单例创建的字典,保证了唯一性。
![](https://img.haomeiwen.com/i16702189/8d1b3c3fbf07e40a.png)
其次,两个
do{...}while()
循环,循环条件都是追溯currentClass
的继承链,也就是被hook的对象所属的类的继承链。
所以,对元类对象(类)的hook的可执行条件,就是两个do{...}while()
循环中的逻辑。
(5). 先看第二个do{...}while()
循环。
想要进入到第二个
do{...}while()
循环,则必定不会在第一个do{...}while()
循环的时候出现return
的现象。之所以先看第二个循环,是因为我们需要知道AspectTracker
到底是作为一个怎样的类。
![](https://img.haomeiwen.com/i16702189/c424eb5e9e3135cf.jpg)
这里我们可以知道 :
- 父类的
tracker
对象中,parentEntry
属性是子类的tracker
对象。- 对于元类对象来说,只要子类被hook,其继承链上的所有父类的
tracker
的selectorNames
集合都会存储被hook的方法名称。AspectTracker
是一个追踪器。
trackerClass
表示被追踪的类。selectorNames
存储被hook的类中被hook的方法的名称,被hook的类的父类的tracker
的selectorNames
集合也会存储被hook的子类的被hook的方法的名称。parentEntry
存放下一级别子类的追踪器。
![](https://img.haomeiwen.com/i16702189/f4ff1c6a0696d7dd.jpg)
![](https://img.haomeiwen.com/i16702189/1035f258108825db.jpg)
(6). 再看第一个do{...}while()
循环。
先看第一个do{...}while()
的整体结构。
![](https://img.haomeiwen.com/i16702189/c5214ed2b926ecfa.jpg)
再看其中最重点的if
结构。
![](https://img.haomeiwen.com/i16702189/80c5f1ef8bb9d402.jpg)
从图2.3.13
中可以得出 :
每个类的层次结构或者说继承链中,相同的方法只能被该层次中的类hook一次,不能同时多次hook同一方法。
注释
校验之后的核心内容,将会放入下一节,AOP之Aspects库(二)进行探索。