Method Swizzling 是什么?
Method Swizzling的含义是方法交换,其核心内容是使用runtime api
在运行时将一个方法的实现替换成另一个方法的实现。我们利用它可以替换系统或者我们自定义类的方法实现,进而达到我们的特殊目的,这就是我们常说的iOS黑魔法。
本文Demo地址:Github-JQMethodSwizzling
Method Swizzling 原理
OC方法在底层是通过方法编号SEL
和函数实现IMP
一一对应进行关联的。打个比方,OC类好比一本书,SEL
就像是书中的目录,IMP
相当于每条目录所对应的页码。关系如图所示:
方法交换的代码如下:
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swiSEL);
method_exchangeImplementations(oriMethod, swiMethod);
交换后的关系如图所示:
从上述方法交换可以看出:
- 交换的是两者的方法实现,也就是说调用
oriSEL
方法时,最终走的方法实现是swiIMP
;调用swiSEL
方法时,最终走的方法实现是oriIMP
。 - 由此可见,在进行方法交换操作时,如果交换代码调用了两次或多次(2的倍数),就会导致方法实现又交换了回去,相当于交换了个寂寞,所以交换代码建议放在单例下进行来保证方法交换的有效性。
方法交换在使用中的递归调用分析
首先,我们来创建一个JQStudent
类,类中有两个实例方法,jq_studentInstanceMethod
和studentInstanceMethod
;然后,在load
方法中对两个方法进行交换;最后,jq_studentInstanceMethod
的实现中再次调用jq_studentInstanceMethod
方法。
代码实现如下图:
我们看到,这里会在jq_studentInstanceMethod
方法中再次调用该方法,会不会引起递归调用呢?
运行结果如下图:
从运行结果看,并没有引起递归。这是因为进行方法交换后,在执行[st studentInstanceMethod]
时,实际上找到的是jq_studentInstanceMethod
的方法实现,而jq_studentInstanceMethod
方法实现中又执行[self jq_studentInstanceMethod]
,同样是因为方法交换,此时jq_studentInstanceMethod
的方法实现也已经指向了studentInstanceMethod
,所以并不会引起递归调用。相反,如果我们在jq_studentInstanceMethod
方法中调用了[self studentInstanceMethod]
才是会引起递归调用的,小伙伴们一定要注意!!!
流程如下图:
在实际的开发中,我们常采用这种方式对业务流程中的一些关键方法进行方法交换(俗称hook
),从而达到不影响业务流程的情况下完成一些信息的收集工作,而这种方式则被称为AOP
(Aspect Oriented Programming
,面向切面编程)。AOP
是一种编程的思想,区别于OOP
(Object Oriented Programming
,面向对象编程)。其实OOP
和AOP
都是一种编程的思想,只不过OOP
编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元。而AOP
则是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性。
方法交换的坑点和分析
坑点一:交换父类的方法
我们还在刚才的Demo中来演示,现在有一个JQStudent
类了,再创建一个JQPerson
类,让JQStudent
继承JQPerson
,在JQPerson
类添加一个实例方法personInstanceMethod
,在JQStudent
类的load
方法中将jq_studentInstanceMethod
方法和父类中的personInstanceMethod
方法进行交换。
实现代码如下图:
运行结果如下图:
从上面的结果可以看到:
- 子类
JQStudent
对象调用父类JQPerson
的方法personInstanceMethod
,消息发送会通过方法查找从而找到父类方法并调用。 - 但是此时父类
JQPerson
中的方法personInstanceMethod
对应的方法实现已经被交换成了子类JQStudent
的jq_studentInstanceMethod
,因此会执行子类的jq_studentInstanceMethod
方法实现。 - 同理,此时子类中调用
jq_studentInstanceMethod
方法,会执行父类的personInstanceMethod
方法实现。
这样看起来好像没有什么问题啊!紧接着,我们再使用父类JQPerson
对象调用一下personInstanceMethod
方法,如下图:
啪、啪、啪,报错了!!!我们来分析下什么原因,
- 首先,父类调换用
personInstanceMethod
方法会执行子类中的jq_studentInstanceMethod
方法实现。 - 然后又调用了
jq_studentInstanceMethod
方法,但是,此时的调用者是JQPerson
对象,父类JQPerson
中并没有jq_studentInstanceMethod
方法实现。所以因方法找不到而报错。
出了问题,我们来解决以下,将交换方式换成下面这种:
再看运行结果:
此时,我们的运行不报错了,而且JQStudent
对象调用父类的personInstanceMethod
方法,确实走了方法交换后的流程,JQPerson
对象也正常的调用了personInstanceMethod
方法,互不影响。为什么呢?
原因是:
- 在方法交换前,先尝试给本类添加一下
oriSEL
方法,方法实现为swiMethod
; - 如果添加成功则返回
YES
,代表本类中原本没有oriSEL
的方法实现;接着,再将父类的方法实现oriMethod
替换给本类的swiSEL
; - 添加失败则返回
NO
,代表本类中已有oriSEL
的方法实现,进行正常的方法交换即可。
坑点二:交换的父类中并没有实现的方法
如果要交换的父类方法并没有实现呢?直接看下运行结果:
什么情况?我的天,递归了!!!为什么呢?我们断点调试一下,看图解释:
从上面这些坑中,我们可以得出一些结论:
- 方法交换要遵循功能单一原则,也就是说本类交换本类中的方法,不能影响父类,否则会影响父类和兄弟姐妹的行为(方法);
- 即使要交换父类的方法,也要在本类中实现(重写)父类的方法;
- 本类或父类交换的方法实现不存在,要给本类添加这个方法实现,否则会出现递归调用
基于以上特点,我封装一个更好的方法交换方式,请看以下代码实现:
运行结果如下: