Hook方法简介
Swift的方法调度
Swift的方法调度有3种不同方式:
- 直接派发(Direct Dispatch)
- 函数表派发(Table Dispatch)
- 消息派发(Message Dispatch) 关于这些方式的说明,网上有许多文章介绍,本文不再赘述。
过程说明
本文探讨的Hook方法,针对后两种调度方式( 函数表派发 和 消息派发 ),不需要区分具体属于哪种类型,过程如下
- 创建一个 Target类 的子类( Hook类 )
- 重写目标函数
- 通过调用 super函数 的方式调用 origin函数
- 执行Hook操作 PS: 直接在内存中修改Class对象属性,会导致意外的问题,需要手动创建一个中间的空子类( Empty类 ),具体原因下文会说明
protocol DDSwiftHookable: AnyObject {
}
extension DDSwiftHookable {
static func enableHook() {
// do hook
}
}
/**
* Demo
*/
class TargetClass {
func targetFunction() {
// do something
}
}
class TargetClassEmpty : TargetClass { // An empty class for save objc origin function
}
class TargetClassHook : TargetClassEmpty, DDSwiftHookable {
override func targetFunction() {
super.targetFunction(); // call origin function
}
}
TargetClassHook.enableHook(); // do hook
原理简介
Swift的子类会继承对应函数的调度方式,所以通过重写函数,可以不用区分具体的调度方式,由编译器自动保持对齐,只需要保证以下两个问题:
- 外部调度时,将 Target类 的函数地址替换成 Hook类 函数地址
- Hook类 函数调度 super函数 时,能正确调度origin函数 针对不同的调度方式,以上两个问题,采取不同的处理方式
函数表派发
问题 1
函数表的设计机制,保证同个函数在 Target类 和 Hook类 中的偏移量是一致,所以只需要用 Hook类 的函数表直接覆盖 Target类 的,就能修改外部调用。
问题 2
在 函数表派发 的情况下,调用 super函数 时,是 直接派发 的,所以 Hook类 的 super函数 就是 origin函数 ,所以不需要额外的处理。
PS:基于这个原因,本方案在 函数表派发 情况下,无法Hook到 Target类 子类的函数
消息派发
问题 1
直接利用Objc的动态机制,直接替换 Target类 的函数地址。
问题 2
对于问题2, 消息派发 的方式会通过调用_objc_msgSendSuper2,动态调度 super函数 。但是传给_objc_msgSendSuper2的 Hook类 是静态指针对象,无法动态替换。所以本方案采取中 Target类 和 Hook类 的继承关系中加多一个 Empty类 ,用来存储 origin函数 ,这样_objc_msgSendSuper2就会在 Empty类 中找到origin函数。
更多
Protocol
Protocol的调度是基于witness table的,而witness table存储的函数则是由编译器生成的中间函数,再通过中间函数去调用相应的函数,属于一个外部调用,所以本方案也能覆盖相关的情况。
范型
由于范型的继承,父类和子类的 generic参数 是存储在不同属性中。因此,无法用范型的 Hook类 来实现Hook。简单的理解就是, Hook类 的函数可能访问自己的 generic参数 ,这样操作发生在 Target类 上,就会越界。
方案优劣
优点
- 不需要区分具体实现是函数表派发,还是消息派发
- 对于 函数表派发 情况,不需要定位 origin函数 在表中的位置
- 支持同一函数同时支持 函数表派发 和 消息派发 的情况
缺陷
- 目标类的子类,基于 函数表派发 的函数,是无法Hook到
- 在同一函数同时支持 函数表派发 和 消息派发 的情况下,子类的不同方式调用,会出现两种情况
- 对于范型支持,只能做到用具体类型的 Hook类 去Hook具体类型的 Target类