有时候因为各种需求,我们可能需要给某些系统类或者SDK的类的方法中,添加某些自己的代码。这时候我们就需要用到 Method-swizzling
。
方法交换
简单来说就是通过runtime
中的method_exchangeImplementations
方法,交换2个方法的sel指向,让他们指向另一个imp。
例如:有一个BClass
类,其中有func1
方法
@implementation BClass
- (void)func1 {
NSLog(@"BClass: func1");
}
@end
复制代码
现在我们给他添加一个分类,在分类中用func2
替换func1
的实现
@implementation BClass (MethodSwizzling)
+ (void)load {
Method original = class_getInstanceMethod(self, @selector(func1));
Method replace = class_getInstanceMethod(self, @selector(func2));
method_exchangeImplementations(original, replace);
}
- (void)func2 {
NSLog(@"BClass: func2");
}
@end
复制代码
现在当我们使用func1
时
BClass *t = [BClass new];
[t func1]; // 输出 BClass: func2
[t func2]; // 输出 BClass: func1
复制代码
可见现在的sel与imp的关系是:
而原始关系是:
给方法添加内容
上面的方法在调用func1
是,直接执行了func2
的实现,但一般我们是想在func1
方法执行之前或之后,加一些自己的操作,还是需要执行func1
的实现的。 这个其实也很好实现,我们只需要在func2
中调用一下func1
就好了。
- (void)func2 {
NSLog(@"func1 之前");
[self func2];
NSLog(@"func1 之后");
}
复制代码
现在当我们使用func1
时
BClass *t = [BClass new];
[t func1];
打印:
func1 之前
BClass: func1
func1 之后
复制代码
这里需要注意的是,
func2
中调用func1
时写的是[self func2];
,这是因为上面进行了方法交换,现在通过func2
的sel才能找到func1
的imp。
优化方法交换
上面虽然实现了我们的需求,但是不够健壮,有些时候可能会出现bug。 我们先修改一下func2
,方便观察打印
- (void)func2 {
NSLog(@"BClass: func2");
[self func2];
}
复制代码
防止多次调用load,导致方法交换还原
load
方法是可以代码调用的,万一有人代码调用了一下,那我们的交换就会再次交换,也就还原了。虽然这个可能性非常小,但也有可能的。
BClass *t = [BClass new];
[t func1];
/*
打印
BClass: func2
BClass: func1
*/
[BClass load];
[t func1];
/*
打印
BClass: func1
*/
复制代码
对此,我们可以给代码加上仅能执行一次的防御,这样就不会出现交换回去的情况了。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(func1));
Method replace = class_getInstanceMethod(self, @selector(func2));
method_exchangeImplementations(original, replace);
});
}
复制代码
存在继承关系的类的方法
现在让BClass
继承自AClass
,然后func1
是AClass
的方法,BClass
中没有实现。
@implementation AClass
- (void)func1 {
NSLog(@"AClass: func1");
}
@end
@interface BClass : AClass
@end
复制代码
我们使用一下
BClass *t = [BClass new];
[t func1];
/*
打印
BClass: func2
BClass: func1
*/
AClass *a = [AClass new];
[a func1];
/*
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[AClass func2]: unrecognized selector sent to instance 0x600003bb8520'
*/
复制代码
发现当我们使用AClass
的实例,调用func1
时会报错,找不到func2
的实现。
其实原因也很简单,因为BClass
为实现func1
,所以获取Method
的时候,拿到的其实是AClass
的func1
,也就是说把AClass
的func1
与BClass
的func2
进行了方法交换。所以BClass
的实例使用正常,而AClass
的实例使用就错误了。
显然,这种存在继承关系的类方法很常见的。所以我们进行方法交换的时候,不能简单的直接交换,要保证交换的原方法一定是当前类,而不是父类的。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSEL = @selector(func1);
SEL replaceSEL = @selector(func2);
Method original = class_getInstanceMethod(self, originalSEL);
Method replace = class_getInstanceMethod(self, replaceSEL);
// 尝试向BClass中添加一个 func1 的 sel,指向 fun2 imp 的方法
BOOL addMethod = class_addMethod(self, originalSEL, method_getImplementation(replace), method_getTypeEncoding(replace));
// 如果添加成功,说明类本身没有实现 func1,添加失败,则表示类本身实现了 func1
if (addMethod) {
// 类原来未实现 func1,但是我们添加了一个 imp 是 func2 的 func1
// 所以,我们只要再让 func2 的 sel 指向 func1 的 imp,其实就能完成方法交换了
class_replaceMethod(self, replaceSEL, method_getImplementation(original), method_getTypeEncoding(original));
}else{
// 既然类实现了 func1 ,那么可以直接交换了
method_exchangeImplementations(original, replace);
}
});
}
复制代码
如果原方法未实现,上面优化后的交换会导致死循环
如果AClass
的func1
只是声明了,并未实现,BClass
也未实现,那么
BClass *t = [BClass new];
[t func1];
/*
一直打印 BClass: func2
*/
复制代码
这其实是因为,func1
的 imp
,不存在,导致 class_replaceMethod
方法更改 func2
指向失败,func2
依旧指向原来的 imp
导致的
所以,我们可以给 func1
添加一个空的实现
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSEL = @selector(func1);
SEL replaceSEL = @selector(func2);
Method original = class_getInstanceMethod(self, originalSEL);
Method replace = class_getInstanceMethod(self, replaceSEL);
// 如果原方法未实现,给添加一个空的实现
if (!original) {
// 添加方法,注意这里因为original,不存在,所以使用replace的信息添加的方法,这是的imp是指向 replace 的
class_addMethod(self, originalSEL, method_getImplementation(replace), method_getTypeEncoding(replace));
// 重新设置 imp,这个很关键,因为这里originalSEL才指向了新的imp,才能正常进行下面的方法交换
method_setImplementation(replace, imp_implementationWithBlock(^(id self, SEL _cmd){}));
}
BOOL addMethod = class_addMethod(self, originalSEL, method_getImplementation(replace), method_getTypeEncoding(replace));
if (addMethod) {
class_replaceMethod(self, replaceSEL, method_getImplementation(original), method_getTypeEncoding(original));
}else{
method_exchangeImplementations(original, replace);
}
});
}
复制代码