Method swizzling是通过runtime实现的方法交换,能够帮助在原有的方法里增加功能。以UIViewController的viewWillAppear:举例,一般我们交换方法新建一个UIViewController的category,然后在+load方法里面进行方法交换,一般有下面两种写法:
写法一:
#import
@implementation UIViewController (Swizzling)
+ (void)load {
Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzling = class_getInstanceMethod(self, @selector(xxx_viewWillAppear:));
method_exchangeImplementations(original, swizzling);
}
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
}
@end
写法二:
#import
@implementation UIViewController (Swizzling)
+ (void)load {
Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzling = class_getInstanceMethod(self, @selector(xxx_viewWillAppear:));
BOOL successAdded = class_addMethod(self, @selector(viewWillAppear:), method_getImplementation(swizzling), method_getTypeEncoding(swizzling));
if (successAdded) {
class_replaceMethod(self, @selector(xxx_viewWillAppear:), method_getImplementation(original), method_getTypeEncoding(original));
} else {
method_exchangeImplementations(original, swizzling);
}
}
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
}
@end
两种方法代码运行之后均没有问题,我们暂且先来看另一种情况,继承UIViewController创建一个子类CustomViewController,然后添加个一个CustomViewController的分类注意将之前的UIViewController(Swizzling)文件删除
,此时项目中的文件结构如下
同样的按之前的写法一
进行方法交换
接来下我们换成写法二
的方式再试一次
运行之后一切正常,我们分析一下
写法一
和
写法二
的差别就在
写法二
先给类添加方法,添加成功就
替换
它的实现,添加失败则进行
方法交换
,在CustomViewController中,我并没有重写viewWillAppear:方法,所以如果此时直接将两个方法进行交换,实际上是将它的父类UIViewController的viewWillAppear和xxx_viewWillAppear方法进行交换。
而UIViewController是所有VC的父类,当ViewController即将出现的时候,viewWillAppear会调用(注意ViewController没有重写viewWillAppear,故此时是调用父类UIViewController的方法),因为交换了方法实现,此时就是调用到CustomViewController(Swizzling)分类中的xxx_viewWillAppear方法,而xxx_viewWillAppear是属于CustomViewController的方法,所以会出现之前的
[ViewController xxx_viewWillAppear:]: unrecognized selector sent to instance 0x7f8f53406140
改用写法二
之后,首先先向CustomViewController添加viewWillAppear方法,添加成功后直接替换它的方法实现,如果添加失败说明这个类本身就有该方法,则直接进行方法交换。用这种方式无论类中是否有这个方法,都能保证交换的方法是在本类中的方法,而不会影响到父类方法的交换,因此就保证了安全。
如果你以为上面的写法就没有任何问题,我只能说你图样图森破。我们来看另一种场景,UIViewController+Swizzling中交换viewWillAppear, CustomViewController+ Swizzling(CustomViewController中没实现viewWillAppear方法)中交换viewWillAppear,并且都打印NSLog(@"%@",[self Class]);我们期望的打印顺序是:
UIViewController
CustomViewController
然而实际的打印可能是:
CustomViewController
注意我这里用的是可能,我们是在+ (void)load方法进行方法交换,load方法的加载能保证父类优先,然后再是子类,但是分类的load加载时机没有固定顺序,这个和target->build phases->complie Sources的文件顺序有关,按照从上到下的顺序加载,所以如果CustomViewController+Swizzling在UIViewController+Swizzling上面,即先加载的话,则CustomViewController会先addMethod,此时是copy了父类的方法添加进来然后交换,交换的是原始父类的实现。然后UIViewController+Swizzling加载,交换的方法不会影响CustomViewController+Swizzling,所以此时的打印就是只有
CustomViewController
所以要想达到预期的效果,一个是调整target->build phases->complie Sources的文件顺序,另一个就是当子类没有重写父类方法时,我们在交换的方法中去动态查找父类的实现,下面看下代码如何实现。
首先在CustomViewController+Swizzling定义了一个全局block
#import "CustomViewController+Swizzling.h"
#import
static void (^callOriginalViewWillAppearBlock)(id,SEL,BOOL);
@implementation CustomViewController (Swizzling)
然后在class_addMethod成功后设置这个全局block
+ (void)load {
Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzling = class_getInstanceMethod(self, @selector(custom_viewWillAppear:));
BOOL successAdded = class_addMethod(self, @selector(viewWillAppear:), method_getImplementation(swizzling), method_getTypeEncoding(swizzling));
if (successAdded) {
class_replaceMethod(self, @selector(custom_viewWillAppear:), method_getImplementation(original), method_getTypeEncoding(original));
//重点在这里,重点在这里,重点在这里
void(^callOriginalBlcok)(id,SEL,BOOL) = ^(id self, SEL cmd,BOOL animated) {
//拿到父类
Class superClass = class_getSuperclass([self class]);
Method original = class_getInstanceMethod(superClass, cmd);
//获取父类的函数指针
IMP originalIMP_ = method_getImplementation(original);
//强转函数指针的类型
void(*originalIMP)(id,SEL,BOOL);
originalIMP = (__typeof(originalIMP))originalIMP_;
//函数调用
originalIMP(self,cmd,animated);
};
callOriginalViewWillAppearBlock = callOriginalBlcok;
} else {
method_exchangeImplementations(original, swizzling);
}
}
首先是拿到它的父类,获取父类的Method以及函数指针IMP,进入xcode看下IMP的结构
因为xcode做了一些限制,我们使用IMP时的类型是上图中标签1处的类型,所以我们这里强转一下类型,然后拿到函数指针之后调用。
最后是交换的@selector方法
- (void)xxx_viewWillAppear:(BOOL)animated {
if (callOriginalViewWillAppearBlock) {
callOriginalViewWillAppearBlock(self,@selector(viewWillAppear:),animated);
} else {
[self xxx_viewWillAppear:animated];
}
NSLog(@"CustomViewController category");
}
判断全局block是否为nil,不为nil,则表示动态查找父类的实现;为nil,则直接调用。
这里只是简单介绍了一下正确swizzle的思路,还有很多不完善的地方,推荐大家去看RSSwizzle,swizzle思路也是动态查找父类的实现,但它的实现方式十分优雅,非常值得一看。最后,Method swizzling虽然能给我们带来很多便捷,但是调式困难,以及使用不当带来的麻烦也是难以排查,所以还是谨慎使用。