Method swizzling 用于改变一个已经存在的 selector 的实现。因为 method swizzling 可以通过交换 selector 来改变函数指针的引用。
Method swizzling 原理
在 OC 语言中,SEL 表示一个方法的名字,定义为如下结构体指针:
1 | typedef struct objc_selector *SEL; |
IMP 是一个函数指针类型,指向函数的实现代码,定义如下:
1 | typedef id (*IMP)(id, SEL, ...); |
这个被指向的函数的参数包含一个接收消息的 id 对象、 SEL、以及不定个数的参数,并返回一个 id 类型的对象,我们可以像在C语言里面一样使用这个函数指针。
SEL 与 IMP 的关系非常类似于 HashTable 中 key 与 value 的关系。OC 中不支持函数重载的原因就是因为一个类的方法列表中不能存在两个相同的 SEL 。但是不同的类中可以拥有相同的 SEL,不同类的实例对象执行相同的 SEL 时,会在各自的方法列表中去根据 SEL 去寻找自己对应的IMP。这使得OC可以支持函数重写。
method swizzling 技术使两个 SEL 所对应的 IMP 相互交换,也就是交换了 SEL1 和 SEL2 的函数实现,如下图所示:
method swizzling 涉及到的 runtime 接口主要有以下几个:
1 | //获取一个类中的实例方法 Method |
⚠️ 需要注意的是, class_getMethodImplementation(cls, name)
要比 method_getImplementation(class_getInstanceMethod(cls, name))
快。
Method swizzling 应用
1. 替换一个类的实例方法
example: 页面打点,替换 NSViewController 的 viewWillAppear 方法
1 | #import "NSViewController+Tracking.h" |
现在,NSViewController 或其子类的实例对象在调用 viewWillAppear: 的时候会有 log 的输出:
1 | 2020-05-08 21:40:39.495648+0800 Demo[85619:1059256] tracking viewWillAppear: <ViewController: 0x600002c1cd00> |
在视图控制器的生命周期,响应事件,绘制视图或者 Foundation 框架的网络栈等方法中插入代码都是 method swizzling 能够为开发带来好的作用的例子。有很多的场景选择 method swizzling 会是很合适的解决方式,这显然也会让 Objective-C 开发者的技术变得越来越成熟。
在进行 method swizzling 的时候需要注意以下几点⚠️
- swizzling 应该只在 +load 中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。
- swizzling 应该只在 dispatch_once 中完成。
由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once 满足了所需要的需求,并且应该被当做使用 swizzling 的初始化单例方法的标准。 - 多个有继承关系的类进行 swizzling 时,应该按照由父类到子类的顺序开始,这样才能保证子类方法拿到父类中的被 swizzled 的实现。在
+(void)load
中 swizzle 不会出错,就是因为 load 类方法会默认从父类开始调用。
有许多人认为交换方法实现会带来无法预料的结果。然而采取了以下预防措施后, method swizzling 会变得相对可靠:
- 在交换方法实现后记得要调用原生方法的实现(除非你非常确定可以不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。
- 避免冲突:为分类的方法加前缀,一定要确保调用了原生方法的所有地方不会因为你交换了方法的实现而出现意想不到的结果。
- 理解实现原理:只是简单的拷贝粘贴交换方法实现的代码而不去理解实现原理不仅会让 App 很脆弱,并且浪费了学习 Objective-C 运行时的机会。阅读 Objective-C Runtime Reference 并且浏览 <obje/runtime.h> 能够让你更好理解实现原理。
- 持续的预防:不管你对你理解 swlzzling 框架,UIKit 或者其他内嵌框架有多自信,一定要记住所有东西在下一个发行版本都可能变得不再好使。做好准备,在使用这个黑魔法中走得更远,不要让程序反而出现不可思议的行为。
2. hook 一个“黑盒”类的实例方法(拿不到该类的头文件,只知道该类的名字和要hook的函数名字)
example: hook 一个类 Person 中的 -(void)speak:eat:
方法,但是没有该类的头文件,需要创建一个类(Person_hook)作为载体,将原始方法 -(void)speak:eat:
hook到该载体类的方法 -(void)hook_speak:eat:
中。
具体实现如下,新建一个类 Person_hook:1
2
3
4
5
6//Person_hook.h
#import <Foundation/Foundation.h>
@interface Person_hook : NSObject
@end
1 | //Person_hook.m |
当原始类 Person 的 speak:eat:
方法在某个地方被调用时,就会被 hook 到 Person_hook 类中的方法,从而实现 hook 原始类方法的目的。控制台会打印如下,其中第二行是原始方法的行为,第一行和第三行是我们把它hook住以后所做的事情:1
2
32020-05-09 16:16:33.997741+0800 Demo[6117:1349426] 即将调用 Person 类的 speak:eat: 方法
2020-05-09 16:16:36.938236+0800 Demo[6117:1349426] Person speak: hello, eat: food
2020-05-09 16:16:36.938373+0800 Demo[6117:1349426] 已经调用 Person 类的 speak:eat: 方法
如果上述例子中被hook的实例方法变为类方法,对应的代码有两处不同:
- 获取 Method 的接口变为:
class_getClassMethod
- 类方法保存在元类的 method-list 中,向一个类中动态添加类方法,就是将方法添加到元类 Meta Class 中,上述代码改动两行即可:
1 | Class meta_class = objc_getMetaClass(class_getName(originClass)); |
3. 数组越界防护.
数组/字典是以类蔟方式实现的,NSArray 和 NSMutableArray 并不是数组真实的类名。在进行hook时注意需要拿到真正的类名称。因为在苹果内部实现中,其真实的类名是有可能发生变化的,因此不建议写死。
1 |
|
⚠️ Method swizzling 的危险性
在项目中还是要慎重使用的,使用不当带来的问题,将会让你付出很大代价去排查。Method swizzling 的危险性可以参考以下文章: