原文链接:https://www.cnblogs.com/mddblog/p/11105450.html
如果对方法交换已经比较熟悉,可以跳过整体介绍,直接看常见问题部分
整体介绍
方法交换是runtime的重要体现,也是"消息语言"的核心。OC给开发者开放了很多接口,让开发者也能全程参与这一过程。
原理
oc的方法调用,比如[self test]
会转换为objc_msgSend(self,@selfector(test))
。objc_msgsend会以@selector(test)
作为标识,在方法接收者(self)所属类(以及所属类继承层次)方法列表找到Method,然后拿到imp函数入口地址,完成方法调用。
typedef struct objc_method *Method;
// oc2.0已废弃,可以作为参考
struct objc_method {
SEL _Nonnull method_name;
char * _Nullable method_types;
IMP _Nonnull method_imp;
}
基于以上铺垫,那么有两种办法可以完成交换:
- 一种是改变
@selfector(test)
,不太现实,因为我们一般都是hook系统方法,我们拿不到系统源码,不能修改。即便是我们自己代码拿到源码修改那也是编译期的事情,并非运行时(跑题了。。。) - 所以我们一般修改imp函数指针。改变sel与imp的映射关系;
系统为我们提供的接口
typedef struct objc_method *Method;
Method是一个不透明指针,我们不能够通过结构体指针的方式来访问它的成员,只能通过暴露的接口来操作。
接口如下,很简单,一目了然:
#import <objc/runtime.h>
/// 根据cls和sel获取实例Method
Method _Nonnull * _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
/// 给cls新增方法,需要提供结构体的三个成员,如果已经存在则返回NO,不存在则新增并返回成功
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
/// method->imp
IMP _Nonnull method_getImplementation(Method _Nonnull m);
/// 替换
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
/// 跟定两个method,交换它们的imp:这个好像就是我们想要的
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
简单使用
假设交换UIViewController的viewDidLoad方法
/// UIViewController 某个分类
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
Method originMethod = class_getInstanceMethod(target, originalSelector);
Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
method_exchangeImplementations(originMethod, swizzledMethod);
}
+ (void)load {
[self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
}
/// hook
- (void)swizzle_viewDidLoad {
[self swizzle_viewDidLoad];
}
交换本身简单:原理简单,接口方法也少而且好理解,因为结构体定义也就三个成员变量,也难不到哪里去!
但是,具体到使用场景,叠加上其它外部的不稳定因素,想要稳定的写出通用或者半通用交换方法,上面的"简单使用"远远不够的。
下面就详细介绍下几种常见坑,也是为啥网上已有很多文章介绍方法交换,为什么还要再写一篇的原因:不再有盲点
常见问题一、被多次调用(多次交换)
"简单使用"中的代码用于hook viewDidload一般是没问题的,+load 方法一般也执行一次。但是如果一些程序员写法不规范时,会造成多次调用。
比如写了UIViewController的子类,在子类里面实现+load
方法,又习惯性的调用了super方法
+ (void)load {
// 这里会引起UIViewController父类load方法多次调用
[super load];
}
又或者更不规范的调用,直接调用load,类似[UIViewController load]
为了没盲点,我们扩展下load的调用:
- load方法的调用时机在dyld映射image时期,这也符合逻辑,加载完调用load。
- 类与类之间的调用顺序与编译顺序有关,先编译的优先调用,继承层次上的调用顺序则是先父类再子类;
- 类与分类的调用顺序是,优先调用类,然后是分类;
- 分类之间的顺序,与编译顺序有关,优先编译的先调用;
- 系统的调用是直接拿到imp调用,没有走消息机制;
手动的[super load]
或者[UIViewController load]
则走的是消息机制,分类的会优先调用,如果你运气好,另外一个程序员也实现了UIViewController的分类,且实现+load方法,还后编译,则你的load方法也只执行一次;(分类同名方法后编译的会“覆盖”之前的)
为了保险起见,还是:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
});
}
继续扩展:多次调用的副作用是什么呢?
- 根据原理,如果是偶数次
结果就是方法交换不生效,但是有遗留问题,这时手动调用
- (void)swizzle_viewDidLoad {
[self swizzle_viewDidLoad];
}
会引起死循环。