本文由快学吧个人写作,以任何形式转载请表明原文出处。
下文会将method_swizzling简称为m_s。
一、简述m_s利用的思想
有关method_swizzling,就会想到AOP,简单复述下AOP和OOP。
AOP : 面向切面编程。是一种编程思想,核心是要解耦和提高模块的复用率。将每个模块中相同的部分提取出来进行封装,调用的时候直接调用封装好的。
OOP : 面向对象。主要是对各个业务模块进行封装,划分出更清晰的逻辑单元。
二、什么是m_s
简单的说就是交换两个SEL的IMP。如下图。
经常会在hook的时候使用。在原有方法前再进行一些处理,然后再继续执行原有方法。
三、method_swizzling的使用
1. 防止数组越界导致的崩溃
1. 用objectAtIndex取值
如果我在ViewController的ViewDidLoad中写如下的代码,会发生数组越界的崩溃 :
为了不让程序直接发生崩溃,可以对NSArray的objectAtIndex
方法做一些优化。
但是我们是不能直接操作NSArray的源码的,所以需要通过之前讲过的分类,来变相的解决这个问题。
创建NSArray的分类,并实现NSArray分类的load方法,其他代码如下图 :
再次运行就不会因为越界的问题崩溃 :
越界返回的是nil,所以ViewController中打印的arr[3]
就是null。
2. 用[ ]
语法取值
ViewController代码如下,就是将objectAtIndex
换成常用的arr[]
这样的数组取值方法了。
NSArray(JD)分类中的代码不变,还是对objectAtIndex
进行m_s,但是还会崩溃。
这个reason就说明了为什么给数组交换方法的时候cls要填__NSArrayI
,还讲明了一点,arr[]
这种数组取值方式调用的方法是objectAtIndexedSubscript
。所以想要不崩溃,处理方法和上面一样,只不过要交换的方法换成了objectAtIndexedSubscript
。
代码如下图 :
ViewController不动,NSArray(JD)中代码如下 :
再执行,用arr[]取值,越界也不会崩溃了。
3. 优化一些
如果按照上面那么写,也没有什么问题,但是如果有人就要再手动调用一下NSArray的load方法,那么就会出问题。
因为再调用load的时候,又会进行一次m_s,就相当于又将方法换了回来。为了不影响手动调用load的使用,所以需要将m_s的代码设计成只执行一次 :
在运行就还是正常的了,因为load_images就已经运行过一次load了,自己再调用load,也不会进入dispatch_once里面了。
还可以将m_s的代码提取出来,封装成工具类,这样load里面的代码就不会显得臃肿和凌乱。
则NSArray(JD)里面的代码就可以换成 :
2. 关于类簇简单的说一下
数组是一个类簇,那什么是类簇?
类簇是一种设计模式,类簇中的类利用相同的接口,但是可以有不同的实现。类簇概念链接
OC中常见的类簇如下 :
类名 | 实际名称 |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
3. 关于类和父类的m_s
对于子类来说,会经常调用父类的方法,有的时候可能会用m_s直接对父类的方法做一些优化。那么就会存在一些问题,比如 :
(1). 父类的方法是实现的,子类没有实现这个方法,但是调用了父类的这个方法的实现。并且还进行了m_s。那么就会出现一些问题。举例如下 :
JDKid是JDMan的子类。JDMan实现了study方法,子类没有实现,但是子类调用了study,并且子类还在自己的内部对study进行了m_s操作。
JDMan的代码 :
JDKid的代码 :
ViewController中的代码 :
如果就按照上面的这种,那么在[aMan study]这里必定报错。如下图 :
报错的原因 :
- 因为JDMan的study的IMP被换成了JDKid的jdkid_study的IMP,也就是如下图
- 所以[aMan study]的时候,调用到的IMP是jdkid_study的实现,也就是上图中红框的部分,红框里的[self jdkid_study]这句代码就有问题了,self这时候是aMan,是JDMan的对象,JDMan中是没有jdkid_study的SEL的,也就是说,aMan根本找不到jdkid_study,所以报错
unrecognized selector
。
修改和思路 :
思路 :
- 给子类添加study方法,不要影响父类调用自己的study方法。
- 如果不想重写父类的study方法,有想要子类也可以调用到study,并添加自己的一些逻辑。那么就要用m_s。
修改代码如下 :
运行结果 :
(2). 父类和子类都没实现的方法,被用m_s交换了
将JDMan的study方法的实现从JDMan.m中去掉。再运行,结果如下 :
会一直递归。
递归的原因 :
如下图 :
上面的确是给JDKid添加了一个study的方法,并且JDKid的study方法的实现实际上是jdkid_study的实现。所以aKid调用study的时候,实际上是调用了jdkid_study的实现,在jdkid_study中是有一行[self jdkid_study]的,因为JDMan也没实现study,导致在给jdkid_study换IMP实现的时候,图中的oriMethod是nil的,所以根本换不成,那么jdkid_study的实现就还是原来自己的实现。这就变成了递归。
修改和思路 :
判断一下父类是否实现了,如果没实现,在拿到jdkid_study的实现给到JDKid动态添加的study方法后,手动给jdkid_study更换一个IMP实现。
因为JDMan没有study方法,所以在ViewController中就把[aMan study]注释掉,避免崩溃。
ViewController中的代码 :
修改后的代码 :
再运行就是正常的了 :
(3). 小结
m_s不要影响到原来的类的调用,尽可能的是在原有的方法基础上做一些优化。子类如果没有重写方法,还想在父类的方法中做一些事情,那么就要动态的给子类添加这个方法,去做method_swizzling的也是子类的方法交换,不要影响到父类的使用。
如果是交换类方法,则更改如下 :