什么是method swizzling?
1) Method Swizzling(方法交换),顾名思义,就是将两个方法的实现 交换,即由原来的A-AImp、B-BImp对应关系变成了A-BImp、B-AImp。
2) 每个类都维护一个方法Method列表,Method则包含SEL和其对应IMP的信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系。
3) Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。
4) Method Swizzling是OC动态性的最好诠释,深入地去学习并理解其特性,将有助于我们在业务量不断增大的同时还能保持代码的低耦合度,降低维护的工作量和难度。
交换前
交换后
Method Swizzling相关函数API
1)获取一个OC实现的编码类型
method_getTypeEncoding
2)給方法添加实现
class_addMethod
3)用一个方法的实现替换另一个方法的实现
class_replaceMethod
4)交换两个方法的实现
method_exchangeImplementations
Method Swizzling使用注意事项
1)方法交换应该保证唯一性和原子性
唯一性:应该尽可能在+load方法中实现,这样可以保证方法一定会调用且不会出现异常。
原子性:使用dispatch_once来执行方法交换,这样可以保证只运行一次。
2) 一定要调用原始实现
由于iOS的内部实现对我们来说是不可见的,使用方法交换可能会导致其代码结构改变,而对系统产生其他影响,因此应该调用原始实现来保证内部操作的正常运行
3) 方法名必须不能产生冲突
4) 做好注释和Log
5) 如果非迫不得已,尽量少用方法交换
虽然方法交换可以让我们高效地解决问题,但是如果处理不好,可能会导致一些莫名其妙的bug。
典型坑点-交换方法主动调用load
第一个坑点比较简单,就是我们在load中交换完方法后,不做处理的话,如果再去调用load,方法IMP会又被交换回来,导致交换不成功。
解决的方法也比较简单,在上述的注意事项1中已经说过,使用单例模式来交换方法,保证方法的交换只执行一次.(如果可以确定不主动调用load就不要加入单例,因为load本身就是一种锁)
无侵入埋点
在 iOS 开发中最常见的三种埋点,就是对页面进入次数、页面停留时间、点击事件的埋点。这些都可以通过Method Swizzling来实现。
下面的例子中,我们通过交换UIViewController中viewWillAppear和viewWillDisappear的方法,来实现了进入界面和退出界面的统计,并记录了相关的类名,通过映射的关系,就可以清楚的知道用户的行为了.
对页面进入次数埋点
首先创建一个UIViewController 的分类
注意一定不要忘记在交换的新方法中再执行原先的方法,保证操作不影响其他功能。
点击事件的埋点
和 UIViewController生命周期埋点不同的是,UIButton在一个视图类中可能有多个不同的继承类,相同 UIButton的子类在不同视图类的埋点也要区别开。所以,我们需要通过 “action 选择器名 NSStringFromSelector(action)” +“视图类名 NSStringFromClass([target class])”组合成一个唯一的标识,来进行埋点记录,实现按钮防止重复点击
创建UIController 分类
在需要的按钮处调用
除了 UIViewController、UIButton 控件以外,Cocoa 框架的其他控件都可以使用这种方法来进行无侵入埋点。以 Cocoa 框架中最复杂的 UITableView 控件为例,你可以使用 hook setDelegate 方法来实现无侵入埋点。另外,对于 Cocoa 框架中的手势事件(Gesture Event),我们也可以通过 hook initWithTarget:action:方法来实现无侵入埋点。
防止数组,字典等越界崩溃
在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇(Class Clusters),一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行Swizzling,必须获取到其真身进行Swizzling,直接对NSArray进行操作是无效的。这是因为Method Swizzling对NSArray这些的类簇是不起作用的。
因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作,如果用下标访问,还需要处理objectAtIndexedSubscript
所以如果我们对NSArray类进行Swizzling操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行Swizzling操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。
NSArray本类类名
数组的所有有关真类:
不可变数组元素个数大于1:__NSArrayI
不可变数组元素个数为0 : __NSArray0
不可变数组元素个数为1 =__NSSingleObjectArrayI
可变数组: __NSArrayM
创建NSArray 分类交换数组所有情况的objectAtIndex 和objectAtIndexedSubscript(下标访问),注意,针对 __NSArray0 空数组只交换objectAtIndex 即可,即使你用下标访问空不可变数组,它调用的仍是objectAtIndex 。
如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
以上的两个例子,只是开发中常用的,还有很多其他的应用,就需要根据需求来不断调整了。这些都属于AOP面向切面编程的一个实际应用,Method Swizzling也是其在iOS开发中应用的最常用的一种AOP思想。