Aspects 是一个提供健壮 AOP 能力的库。它的实现原理和 JSPatch 类似。也真是如此,它和 JSPatch 混用时会产生冲突。
使用
Aspects 提供了两个方法来实现三种类型的 hook:
1 | @interface NSObject (Aspects) |
它能 hook 的类型有:
- 所有实例对象的特定方法
- 特定类方法
- 某个特定实例对象的特定方法
两个方法中,前者处理的是类型 1,2;后者处理的是类型 3。
非常有意思,如果你要 hook 1,比如你有一个类
Test
,你要 hookTest
所有实例对象的-(void)test
方法,你要通过Test
去调用。如果你要 hook 2,比如你要 hook
Test
的类方法+(void)test
,你要通过object_getClass(Test)
去调用。
涉及的类
Aspects 在实现的过程中,涉及到一些类的使用,提前了解它们有助于我们之后的流程分析。
AspectsInfo
1 | @protocol AspectInfo <NSObject> |
这是一个协议,当我们要执行 hook 的方法的时候,需要从实现这个协议的对象中拿到执行必要的信息
AspectIdentifier
1 | @interface AspectIdentifier : NSObject |
AspectIdentifer
包含了 hook 需要的绝大多数信息,配合 AspectsInfo
就可以实现 hook。
AspectsContainer
1 | @interface AspectsContainer : NSObject |
每一个被 hook 的方法会对应一个 AspectsContaienr
。AspectsContainer
保存了不同时机要执行的 hook 信息 AspectsIdentifier
实例。
之后执行的方法判断要执行 hook 的时候,就会拿到 SEL 对应的 AspectContainer
实例,从中拿出一个个 AspectsIdentifier
,再结合通过方法执行时拿到的 NSInvocation 实例创建的 AspectsInfo
实例,达到最终 hook 方法的目的。
AspectTracker
1 | @interface AspectTracker : NSObject |
AspectTracker
的目的是要保存继承链上对于某个方法的 hook 的情况。这是为了在 hook 的时候校验并保证同一个继承链上只能有一个类被 hook。至于这样的目的,后面再细说。
其实这个原因我疑惑了很久,看了很多文章,但是几乎所有对 Aspects 的源码解析都没有对
AspectTracker
的意义进行解释。只是照本宣科的说明继承链上只能有一个类被 hook,而没有说明为什么要设计成这样。
流程
进行 hook
hook 过程涉及的两个方法,一个类方法,一个实例方法都是直接调用 aspect_add()
方法:
1 | /// 添加 hook |
整个过程分为几步:
- 判断是否可以 hook
- 创建
AspectsContainer
实例 - 创建
AspectsIdentifier
实例 - 实现 hook 方法
判断是否可以 hook
hook 前要判断是否可以 hook。hook 的原则有几点:
- 已经被 hook 的方法不会被重复 hook
- 不能 hook 包含 retain,release,autorelease,forwardInvocation 的方法
- hook 方法只能在 dealloc 方法前执行,不能替换 dealloc 方法
- 不能 hook 不存在的 selector
- 不能 hook 继承链上已经被 hook 过的同名方法
1 | /// 检验 aspect 是否可以 hook 该方法。主要规则对于类方法,一个继承链上的同名方法只能被 hook 一次 |
前四个规则都容易理解。那么为什么要有同一个继承链上只能 hook 一次同名方法?我们来看这样一个例子:
1 | @interface A : NSObject |
场景很简单,父类 hook 了某一个方法,子类中调用父类的该方法。熟悉 runtime 的都知道,调用父类方法时,调用者是子类本身,方法实现是执行父类的 IMP。这个时候由于父类进行了 hook,IMP 指向 msg_forward
,因此会进行消息转发。消息转发时,NSInvocation
只知道调用者是子类,并不知道其实调用的是父类方法。因此,还会执行子类方法。也就是说,变成了子类中该方法又调用了自身,进而形成无限循环。
因此,Aspects 的作者禁止了同一个继承链上的多次 hook。当然这是为了解决这种无限循环的无奈之举。禁用某些操作以达到安全性,当然这也限制了一些实际需求的实现。
JSPatch 在执行到 super 方法的时候会判断 super 方法是否被重写,如果被重写了那么执行重写的 JS 方法。而不是像 Aspects 中非常武断的只是不让一个继承链 hook 一次。
创建 AspectsContainer
实例
前面说到,每一个被 hook 的实例或者对象的每一个方法都会有一个 AspectsContainer
以关联对象的方式存储。即方法 aspect_getContainerForObject
1 | static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) { |
方法很简单,就是先查看这个对象上有没有方法名对应的关联对象,有就取出,没有就创建。
这就说明了,类对象和元类对象都是可以设置关联对象的
创建 AspectsIdentifier
实例
AspectsIdentifier
将 hook 的信息保存起来,它会做两件事:
- 获取 block 的签名
- 比较 block 的签名和 hook 的方法签名是否一致
1 | /// 创建一个 AspectIdentifier |
获取 block 签名
获取 block 签名的方式在 jspatch 中有类似的操作。通过 block 的地址,参考 block 结构体的内存分布,将指针移动到 signature 的位置。区别在于 JSPatch hook 的 block 全都是全局 block,因此 block 结构中不会存在 copy 和 dispose 两个方法。而 Aspects 中的 block 有可能是堆 block。当是堆 block 的时候,要移动两个方法指针的大小:
1 | /// 获取 block 的签名 |
AspectBlockRef
的结构如下:
1 | typedef struct _AspectBlock { |
比较 block 的签名和 hook 的方法签名
1 | /// 比较 block 参数类型和方法参数类型是否一致 |
比较两者的签名主要是比较入参的类型要一致。block 和普通方法的不同在于 block 的签名中不存在 SEL。普通方法的签名 index 为 0 的参数是调用者,即 @
,index 为 1 的参数是 SEL,即 :
,而 block 由于不存在 SEL,其 index 为 0 的参数还是调用者,即 @
,而 index 为 1 的参数就是真正的参数了。Aspects 为了将 block 签名和普通方法签名一致,所以做了限制,只要有参数,第一个一定是一个无关的 AspectInfo
实例。
在创建好 AspectsIdentifier
后,将实例存放到 AspectContainer
中。
实现 hook 方法
hook 方法的真正地方在 aspect_prepareClassAndHookSelector
方法中。分为两步:
- 替换类的
forwardInvocation
方法为自己的实现 - 替换方法的实现为
_objc_msgForward
1 | /// hook 方法的地方 |
替换类的 forwardInvocation
实现
替换在 aspect_hookClass
中实现。这是一个比较重要也比较有技巧性的方法。如注释上写的,对于 hook 对象主要区分为三种情况:
- hook 的是类对象(包括元类对象,以下都简称为类对象)
- hook 的是 kvo 对象
- hook 的是实例对象
1 | /// hook 对象 |
[xxx class]
方法和 object_getClass(xxx)
的区别是要了解的:
[xxx class]
当xxx
是实例对象的时候返回的是类对象,当是类对象的时候,返回的是自身object_getClass(xxx)
返回的是 isa 指向的对象。
所以 class_isMetaClass(object_getClass(self))
如果是 true,那就说明 self 是类对象。直接通过 aspect_swizzleClassInPlace
方法替换其 forwardInvocation
实现。
对于类对象来说,一般情况下 [xxx class]
和 object_getClass(xxx)
返回的结果是一致的。但有一种特殊情况就是 KVO 对象,KVO 会动态生成一个类型,但是会重写修改 class 方法返回的结果。因此,[xxx class] !== object_getClass(xxx)
的时候就说明是 KVO 生成的对象。KVO 对象要 hook NSKVONotifing_xxx
的 forwardInvocation
方法。
前面把类对象都排除了,只剩下实例对象 hook 某个方法的情况。那么如何既不影响其他实例,又能 hook 它的一个的方法呢?我们可以拷贝这个类创建一个新类,然后只修改这个新类的这个方法的实现,就和 Linux 中创建新进程一样,也是实现 KVO 的做法。对于新创建的类,我们修改其 forwardInvocation
方法的指向,并通过 object_setClass
将当前实例的 isa 指向新创建的 class。
替换方法的实现为 _objc_msgForward
这一段的过程在上面的注释中已经很清楚了。主要就是添加一个有前缀的方法,将原来的 IMP 指向它,然后将 _objc_msgForward
指向原来的 SEL 。
至此,hook 方法全部完成。
执行
方法执行时会直接进入消息转发流程:
1 | // This is a macro so we get a cleaner stack trace. |
整个过程就是从关联对象中取出 AspectContainer
实例,然后执行其中各个时间点的回调。你可能会很疑惑它获取关联对象和执行的语句:
1 | // 获取 |
为什么既有从对象中获取关联对象,又有从对象的 isa 指向中获取关联对象的情况呢?对于 hook 类来说,实例的执行都应该是通过后者,从类中获取的。但是对于 hook 实例来说,他们的关联对象是存在实例中的。如果既 hook 了实例的某个方法,又 hook 了实例所在的类的同一个方法,那么两份 hook 都应该执行。
如果 hook 时候传入的 options 为 AspectOptionAutomaticRemoval
,那么会在执行完毕后调用 AspectIdentifier
实例的 remove
方法移除 hook。
方法执行过程在 invokeWithInfo
方法中:
1 | /// 执行这个 NSInvocation |
和之前获取参数的时候类似,这里把参数一个个从 NSInvocation 中设置进去。最后通过 invokeWithTarget
执行 block。
总结
Aspects 的核心原理和 JSPatch 是一致的。当然,它比 JSPatch 还是简单了许多。如果你理解了 JSPatch 中消息转发的过程,Aspects 理解起来就很简单了。
思考题
- 如何 hook 类方法?
- 如何 hook 实例对象的方法?
- block 的方法签名和普通方法的方法签名有什么不同?
- 如何拿到 block 的方法签名?
- 为什么 Aspects 中一个方法同一个继承链上只能 hook 一次?可以怎么改进?
- 怎样判断一个实例对象是否进行过 kvo?
- aspect 中怎么判断一个对象的
forwardInvocation
是否已经被 hook 过? - 如何判断一个对象是类对象还是实例对象?
[self class]
和object_getClass(self)
的区别是什么?- 关联对象能否作用于类和元类?
参考
从 Aspects 源码中我学到了什么?(写的太简略了)