本文为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
库
1.2 打开xcworkspace
文件,并且在ViewController.h
文件中导入
1.3 在ViewController.h
中声明方法- (void)testName:(NSString *)str Age:(int)age Sex:(NSString *)sex;
并且在ViewController.m
中实现如下图
1.4 在viewDidLoad
中利用Aspects
库的公开API
对上图中的方法进行hook,并调用它
1.5 观察上述操作之后的举例运行结果
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 AspectToken
遵循NSObject协议,是
Aspects
库的一个令牌,可以用来注销一个aspect
对象,也就是可以注销一个hook。详细情况见下图2.2.1
的注释。
2.3 AspectInfo
这是
Aspect
库的协议。详细情况见下图2.2.2
的注释。
2.4 Aspects
这是
Aspects
对象。
- 它利用了
Runtime
的消息转发机制进行hook。Aspects
对象用来hook的会使系统产生一些性能上的消耗,所以不要使用它对频繁被调用的方法进行hook。Aspects
主要针对的对象是view和controller中的方法。Aspects
对象可以利用hook后返回的AspectToken
进行注销。Aspects
对象是线程安全的。Aspects
是NSObject
的分类,之所以选择做NSObject
的分类,是因为OC中大多数的类都是NSObject
类的子类。Aspects
不支持hook静态方法,也就是我们常说的类方法。
2.5 AspectErrorCode
这是
Aspects
库的公开API发生错误的时候,返回的错误代码。详情见下图2.2.4
注释。
3. Aspects库的入口之Aspects.m文件
操作
commond+鼠标左键
点击我们在举例的时候使用的Aspects
库的公开API,选择进入它的实现。
3.1 公开API(id)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
,可以选择写与不写,第二个参数开始就是被hook的方法的参数,也就是举例中的name
、age
、sex
。如果选择写上这些参数,则这些参数会被加入到block
的签名信息中。你也可以不写block
的参数或者只使用id
参数。error
: 该方法如果发生错误的话,这里会回调错误信息。
方法实现
可以看到,无论是实例方法还是类方法都是调用了
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 整体逻辑
通过
图2.3.2
的源码可以看出,aspect_add()
函数的实现本质的核心都在被收缩起来的block
函数块里面。
3.2.2 aspect_performLocked
这是一把
OSSpinLock
的锁对象,之前在锁的章节说过,它是自旋锁,并且现在它的底层实现是基于os_unfair_lock
的。
3.2.3 aspect_isSelectorAllowedAndTrack
这是验证利用
Aspects
库进行hook的方法(selector
)是否符合Aspects
库的标准,以及对元类对象(也就是类)做改变。
步骤1 : 建立黑名单
步骤2 : 检查黑名单
步骤3 : 额外的检查
这里我们可以得到两个要素 :
- 如果要hook的方法是
dealloc
方法,那么,position
参数只能选择AspectPositionBefore
枚举值。- 如果要hook的方法并没有被要hook的类声明且实现,是不可以被hook的。
步骤4 : 元类对象的操作
(1). 首先,判断被hook的类是不是元类对象(类)
看图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
是一个静态的、通过单例创建的字典,保证了唯一性。
其次,两个
do{...}while()
循环,循环条件都是追溯currentClass
的继承链,也就是被hook的对象所属的类的继承链。
所以,对元类对象(类)的hook的可执行条件,就是两个do{...}while()
循环中的逻辑。
(5). 先看第二个do{...}while()
循环。
想要进入到第二个
do{...}while()
循环,则必定不会在第一个do{...}while()
循环的时候出现return
的现象。之所以先看第二个循环,是因为我们需要知道AspectTracker
到底是作为一个怎样的类。
这里我们可以知道 :
- 父类的
tracker
对象中,parentEntry
属性是子类的tracker
对象。- 对于元类对象来说,只要子类被hook,其继承链上的所有父类的
tracker
的selectorNames
集合都会存储被hook的方法名称。AspectTracker
是一个追踪器。
trackerClass
表示被追踪的类。selectorNames
存储被hook的类中被hook的方法的名称,被hook的类的父类的tracker
的selectorNames
集合也会存储被hook的子类的被hook的方法的名称。parentEntry
存放下一级别子类的追踪器。
(6). 再看第一个do{...}while()
循环。
先看第一个do{...}while()
的整体结构。
再看其中最重点的if
结构。
从图2.3.13
中可以得出 :
每个类的层次结构或者说继承链中,相同的方法只能被该层次中的类hook一次,不能同时多次hook同一方法。
注释
校验之后的核心内容,将会放入下一节,AOP之Aspects库(二)进行探索。