0%

Swizzle的正确姿势(一)

常见Swizzle方案

在OC里如何实现Swizzle?相信大家经过一番Google之后很容易就能实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL JYReplaceMethodWithBlock(Class c, SEL origSEL, SEL newSEL, id block) {

Method origMethod = class_getInstanceMethod(c, origSEL);

// 添加newSEL及对应的实现
IMP newIMP = imp_implementationWithBlock(block);
if (!class_addMethod(c, newSEL, newIMP, method_getTypeEncoding(origMethod))) {
return NO;
}

Method newMethod = class_getInstanceMethod(c, newSEL);

if (class_addMethod(c, origSEL, newIMP, method_getTypeEncoding(origMethod))) {
// 没有origSEL,添加origSEL,使用新实现
// 同时将newSEL替换为旧实现
class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
} else {
// 已经有origSEL,直接交换实现
method_exchangeImplementations(origMethod, newMethod);
}

return YES;
}

上面就是一个初步的Swizzle方案,重点在最后一部分:

1
2
3
4
5
6
7
8
if (class_addMethod(c, origSEL, newIMP, method_getTypeEncoding(origMethod))) {
// 没有origSEL,添加origSEL,使用新实现
// 同时将newSEL替换为旧实现
class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
} else {
// 已经有origSEL,直接交换实现
method_exchangeImplementations(origMethod, newMethod);
}

这里主要做的事情就是将 origSELnewSEL 的实现进行交换。交换之后,在调用 origSEL 方法时,就会走到我们的自定义实现,也就完成了Swizzle。

比如我们想要Swizzle掉 ViewController 类的 viewDidLoad 方法,直接添加下面的代码即可。

1
2
3
4
SEL newSEL = NSSelectorFromString(@"jy_viewDidLoad");
JYReplaceMethodWithBlock([ViewController class], @selector(viewDidLoad), newSEL, ^(ViewController *_self){
NSLog(@"swizzled");
});

添加之后,ViewController 类原始的 viewDidLoad 方法就会被新的 jy_viewDidLoad 方法替换掉。因此在调用 viewDidLoad 方法时,实际执行的内容已经变成自定义的是实现了,即

1
NSLog(@"swizzled");

原始方法调用

如果我们只是想插入一段自定义的代码,执行完之后还是想走原始的实现怎么办呢?我们先来看下现在的SELIMP的对应关系。

1
2
viewDidLoad --> 新的实现
jy_viewDidLoad --> 原始viewDidLoad的实现

可以看到,现在 jy_viewDidLoad 对应的就是原始的实现,直接调用该方法就行了。

1
2
3
4
5
SEL newSEL = NSSelectorFromString(@"jy_viewDidLoad");
JYReplaceMethodWithBlock([ViewController class], @selector(viewDidLoad), newSEL, ^(ViewController *_self){
NSLog(@"swizzled");
((void (*)(id, SEL))objc_msgSend)(_self, newSEL);
});

这样就实现了插入自定义代码后继续执行原始逻辑。

非常简单是不是?一般情况下我们使用上面的Swizzle方案不会遇到什么问题,但是它实际是有漏洞的,可能会造成令人崩溃的bug。

Swizzle带来的问题

我们在ViewController类的 viewDidLoad 方法中加一句log,看下当前的 _cmd

1
2
3
4
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@", NSStringFromSelector(_cmd));
}

执行之后,输出如下:

1
2020-06-21 19:36:44:675767+8000 Demo[14146:9871258] jy_viewDidLoad

怎么回事?我们明明是在 viewDidLoad 方法中打印的 _cmd,怎么就变成了 jy_viewDidLoad 呢?

为了解释这个问题,我们再复习一下Swizzle代码以及SELIMP的对应关系:

1
2
3
4
5
SEL newSEL = NSSelectorFromString(@"jy_viewDidLoad");
JYReplaceMethodWithBlock([ViewController class], @selector(viewDidLoad), newSEL, ^(ViewController *_self){
NSLog(@"swizzled");
((void (*)(id, SEL))objc_msgSend)(_self, newSEL);
});
1
2
viewDidLoad --> 新的实现
jy_viewDidLoad --> 原始viewDidLoad的实现

看到什么了?实际调用 viewDidLoad 方法时,会走到我们自定义的实现,也就是 NSLog(@"swizzled"); 那段代码,在NSLog之后我们又调用了原始的实现,因为 jy_viewDidLoad 对应的原始的实现,所以调用代码为 ((void (*)(id, SEL))objc_msgSend)(_self, newSEL);。 所以在执行原始实现时,_cmd 变成 jy_viewDidLoad

在本文被的例子中,这个问题可能并不严重,因为并没有基于 _cmd 做任何的逻辑。但是在实际场景中,很有可能有方法使用了 _cmd,比如UIKit中的touch转发:

1
2
3
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
forwardTouchMethod(self, _cmd, touches, event);
}

如果我们Swizzle了 touchesMoved:withEvent: 方法,由于 _cmd 发生了变化,实际运行时 forwardTouchMethod 方法内部会找不到这个方法,导致出现crash。

其他问题

这种Swizzle方法除了会改变 _cmd 之外,也会有一些其他问题,比如同时Swizzle了父类和子类的方法,会导致方法的执行不会按照继承关系走,进而出现一系列奇怪的问题。具体可以参考 http://defagos.github.io/yet_another_article_about_method_swizzling/,本文就不再赘述。

更优雅的Swizzle方案

那么该怎么实现Swizzle才不会有上面的问题呢?Swizzle的正确姿势究竟是啥样的?且待下文分解。
(上面的文章链接中实际已经有答案了,着急的可以直接看~)

参考