常见Swizzle方案
在OC里如何实现Swizzle?相信大家经过一番Google之后很容易就能实现。
1 | BOOL JYReplaceMethodWithBlock(Class c, SEL origSEL, SEL newSEL, id block) { |
上面就是一个初步的Swizzle方案,重点在最后一部分:
1 | if (class_addMethod(c, origSEL, newIMP, method_getTypeEncoding(origMethod))) { |
这里主要做的事情就是将 origSEL 和 newSEL 的实现进行交换。交换之后,在调用 origSEL 方法时,就会走到我们的自定义实现,也就完成了Swizzle。
比如我们想要Swizzle掉 ViewController 类的 viewDidLoad 方法,直接添加下面的代码即可。
1 | SEL newSEL = NSSelectorFromString(@"jy_viewDidLoad"); |
添加之后,ViewController 类原始的 viewDidLoad 方法就会被新的 jy_viewDidLoad 方法替换掉。因此在调用 viewDidLoad 方法时,实际执行的内容已经变成自定义的是实现了,即
1 | NSLog(@"swizzled"); |
原始方法调用
如果我们只是想插入一段自定义的代码,执行完之后还是想走原始的实现怎么办呢?我们先来看下现在的SEL
和IMP
的对应关系。
1 | viewDidLoad --> 新的实现 |
可以看到,现在 jy_viewDidLoad 对应的就是原始的实现,直接调用该方法就行了。
1 | SEL newSEL = NSSelectorFromString(@"jy_viewDidLoad"); |
这样就实现了插入自定义代码后继续执行原始逻辑。
非常简单是不是?一般情况下我们使用上面的Swizzle方案不会遇到什么问题,但是它实际是有漏洞的,可能会造成令人崩溃的bug。
Swizzle带来的问题
我们在ViewController类的 viewDidLoad 方法中加一句log,看下当前的 _cmd
1 | - (void)viewDidLoad { |
执行之后,输出如下:
1 | 2020-06-21 19:36:44:675767+8000 Demo[14146:9871258] jy_viewDidLoad |
怎么回事?我们明明是在 viewDidLoad 方法中打印的 _cmd
,怎么就变成了 jy_viewDidLoad 呢?
为了解释这个问题,我们再复习一下Swizzle代码以及SEL
和IMP
的对应关系:
1 | SEL newSEL = NSSelectorFromString(@"jy_viewDidLoad"); |
1 | viewDidLoad --> 新的实现 |
看到什么了?实际调用 viewDidLoad 方法时,会走到我们自定义的实现,也就是 NSLog(@"swizzled");
那段代码,在NSLog之后我们又调用了原始的实现,因为 jy_viewDidLoad 对应的原始的实现,所以调用代码为 ((void (*)(id, SEL))objc_msgSend)(_self, newSEL);
。 所以在执行原始实现时,_cmd
变成 jy_viewDidLoad 了。
在本文被的例子中,这个问题可能并不严重,因为并没有基于 _cmd
做任何的逻辑。但是在实际场景中,很有可能有方法使用了 _cmd
,比如UIKit中的touch转发:
1 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { |
如果我们Swizzle了 touchesMoved:withEvent: 方法,由于 _cmd
发生了变化,实际运行时 forwardTouchMethod 方法内部会找不到这个方法,导致出现crash。
其他问题
这种Swizzle方法除了会改变 _cmd
之外,也会有一些其他问题,比如同时Swizzle了父类和子类的方法,会导致方法的执行不会按照继承关系走,进而出现一系列奇怪的问题。具体可以参考 http://defagos.github.io/yet_another_article_about_method_swizzling/,本文就不再赘述。
更优雅的Swizzle方案
那么该怎么实现Swizzle才不会有上面的问题呢?Swizzle的正确姿势究竟是啥样的?且待下文分解。
(上面的文章链接中实际已经有答案了,着急的可以直接看~)