前言:我是参考 南峰子 的博客加上自己理解写的,原著专辑大家自己可看:http://southpeak.github.io/categories/objectivec/
1,Method Swizzling啥意思
Method Swizzling 直译是"方法调和",理解起来我们可以想成是方法调剂,方法调整。这样就是方法执行的不合适,我们把它调整调整,哈哈!
通过这个功能,能帮助我们更好的理解Runtime的运行机制,即是在运行时绑定方法。其实一般用在想要不修改原有方法(包括系统方法)实现的情况下,替换掉原方法的实现。其实这个功能真的还是蛮吊的,都说是黑魔法。
2,Method Swizzling常用的网络举栗子
举个例子:
实现功能就是全局在在应用中viewDidAppear插入一个统计代码,这个一般我们不用Runtime中的方法就一个一个添加,或者是所有的ViewContoller都继承一个父类,在父类中添加。但是使用RunTime的Method Swizzling就可以轻松实现这个需求
@implementation UIViewController (Swizzling)
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(viewDidAppear:);
SEL diySelector = @selector(diyViewDidAppear:);
Method originalMethod = class_getInstanceMethod([self class], originalSelector);
Method diyMethod = class_getInstanceMethod([self class], diySelector);
//这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
BOOL didAddMethod = class_addMethod([self class], originalSelector, method_getImplementation(diyMethod), method_getTypeEncoding(diyMethod));
//下面就是 交换exchange 两个SEL 的 IMP指针
if (didAddMethod) {
//该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于class_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现
/*如果添加成功
* 添加成功会发生 1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
* 2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)
* 添加成功后 用originalSEL的IMP指针 指向 swizzingSEL的 MP指针
*/
class_replaceMethod([self class], diySelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
/***没有添加成功,那么就是说originalSEL 和 swizzingSEL 实现都存在
* 直接使用method_exchangeImplementations 交换IMP指针就好
**/
method_exchangeImplementations(originalMethod, diyMethod);
}
});
}
-(void)diyViewDidAppear:(BOOL)anmiate{
NSLog(@"ViewDidAppear重新指向");
[self diyViewDidAppear:anmiate];
}
具体代码就是获取ViewController原生的ViewDidAppear 的Method和自定义diyViewDidAppear的Method 指针,然后交换它两 的IMP(方法实现的标准C调用首地址),通俗的讲就是交换他俩的具体实现。
在这个方法中,会发现最后又调用了自身
-(void)diyViewDidAppear:(BOOL)anmiate{
NSLog(@"ViewDidAppear重新指向");
[self diyViewDidAppear:anmiate];
}
是不是会循环调用了,其实不会,因为它俩的实现被交换了,所以调用的diyViewDidAppear:
其实会调用它原生的viewDidAppear :
。我们来验证一下,在ViewController中,我们这样写:
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Student *s = [[Student alloc]init];
// [s performSelector:@selector(introduceSelf)];
// [s performSelector:@selector(swizzingIntroduceSelf)];
//
// NSArray *testArray = @[@"A",@"B",@"C"];
// NSString *tStr = [testArray objectAtIndex:4];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
NSLog(@"我是原生方法");
}
运行一下,打印结果:
2017-01-12 14:42:04.689 MethodSwizzlingTest[4892:436048] ViewDidAppear重新指向
2017-01-12 14:42:04.690 MethodSwizzlingTest[4892:436048] ViewDidAppear原生方法
3,自写类说明Method Swizzling的方法替换
为了更好的说明,这个关系,我们用一个普通自定义的类来做说明,一个是Person类 一个继承Person的Student类:
其中Person实现如下:
@implementation Person
-(void)introduceSelf{
NSLog(@"person:原有实现");
}
-(void)swizzingIntroduceSelf{
NSLog(@"person:调剂方法");
}
@end
Student类的实现
@implementation Student
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSEL = @selector(introduceSelf);
SEL swizzingSEL = @selector(swizzingIntroduceSelf);
Method originalMethod = class_getInstanceMethod([self class], originalSEL);
Method swizzingMethod = class_getInstanceMethod([self class], swizzingSEL);
/******交换指针*******/
//这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
BOOL didAddMethod = class_addMethod([self class],
originalSEL,
method_getImplementation(swizzingMethod),
method_getTypeEncoding(swizzingMethod));
//下面就是 交换exchange 两个SEL 的 IMP指针
if (didAddMethod) {
/*如果添加成功
* 添加成功会发生 1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
* 2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)
* 添加成功后 用originalSEL的IMP指针 指向 swizzingSEL的 MP指针
*/
class_replaceMethod([self class],
swizzingSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
/***没有添加,那么就是说originalSEL 和 swizzingSEL 实现都存在
* 直接使用method_exchangeImplementations 交换IMP指针就好
**/
method_exchangeImplementations(originalMethod, swizzingMethod);
}
//其实上面这个判断可以归纳为,只要我实现了交换方法swizzingMethod,我都进行 Method Swizzling
// if (!class_addMethod([self class], swizzingSEL, method_getImplementation(swizzingMethod), method_getTypeEncoding(swizzingMethod))) {
// method_exchangeImplementations(originalMethod, swizzingMethod);
// }
});
}
-(void)introduceSelf{
NSLog(@"Student:原有实现");
}
-(void)swizzingIntroduceSelf{
NSLog(@"Student:调剂方法");
[self swizzingIntroduceSelf];
}
@end
这里我们实现了introduceSelf 和 swizzingIntroduceSelf的方法实现交换,我们在VC中这样调用:
Student *s = [[Student alloc]init];
[s performSelector:@selector(introduceSelf)];
打印结果:
2017-01-12 15:07:24.333 MethodSwizzlingTest[5232:525878] Student:调剂方法
2017-01-12 15:07:24.334 MethodSwizzlingTest[5232:525878] Student:原有实现
可以看出我们调用的是introduceSelf
,确打印swizzingIntroduceSelf
的实现,最后我们在swizzingIntroduceSelf
中调用自身,会发现它执行的确实'introduceSelf'的实现。我么可以屏蔽最后调用自身的这段代码如下:
-(void)swizzingIntroduceSelf{
NSLog(@"Student:调剂方法");
// [self swizzingIntroduceSelf];
}
打印输出:
2017-01-12 15:14:00.866 MethodSwizzlingTest[5323:547121] Student:调剂方法
会发现,完全实现了替换。
我们这样调用
Student *s = [[Student alloc]init];
[s performSelector:@selector(swizzingIntroduceSelf)];
结果:
2017-01-12 16:17:47.172 MethodSwizzlingTest[6058:706641] Student:原有实现
会发现方法两个实现方法被完全替换。
4,关于didAddMethod 的判断
这篇文章解释的非常好http://www.jianshu.com/p/f6dad8e1b848,可以看看。
交换之前做了class_addMethod判断
关于class_addMethod,这个解释比较好:
class_addMethod为一个类添加方法(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。需要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。也就是说如果class_addMethod返回YES,说明子类中没有方法originalSelector,通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。
//这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
BOOL didAddMethod = class_addMethod([self class],
originalSEL,
method_getImplementation(swizzingMethod),
method_getTypeEncoding(swizzingMethod));
这个是为什么了,其实就是为了防止本类中originalSEL没有实现,人家说是一种保护,我的注释已经写上,如下:
didAddMethod = YES
添加返回(didAddMethod ==YES),必须满足下面的条件:
1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)
这时候swizzingSEL方法的实现就会赋给originalSEL,同时添加originalSEL方法。在添加成功后,我们还需要把originalSEL的实现替换给swizzingSEL,所有就调用了class_replaceMethod
方法,这样就完成交换。
didAddMethod = NO
说明方法未添加成功,这说明自身中已经实现originalSEL方法,所以直接调用method_exchangeImplementations
来完成方法实现的交换
5,Method Swizzling 额外知识(我只是个搬运工)
为什么方法交换调用在+load方法中?
在Objective-C runtime会自动调用两个类方法,分别为+load与+ initialize。+load 方法是在类被加载的时候调用的,也就是一定会被调用。而+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说+initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是永远不会被调用的。此外+load方法还有一个非常重要的特性,那就是子类、父类和分类中的+load方法的实现是被区别对待的。换句话说在 Objective-C runtime自动调用+load方法时,分类中的+load方法并不会对主类中的+load方法造成覆盖。综上所述,+load 方法是实现 Method Swizzling 逻辑的最佳“场所”。如需更深入理解,可参考Objective-C 深入理解 +load 和 +initialize。
为什么方法交换要在dispatch_once中执行?
方法交换应该要线程安全,而且保证在任何情况下(多线程环境,或者被其他人手动再次调用+load方法)只交换一次,防止再次调用又将方法交换回来。除非只是临时交换使用,在使用完成后又交换回来。 最常用的解决方案是在+load方法中使用dispatch_once来保证交换是安全的。之前有读者反馈+load方法本身即为线程安全,为什么仍需添加dispatch_once,其原因就在于+load方法本身无法保证其中代码只被执行一次。
6,Method Swizzling 试用场景
实例一:替换ViewController生命周期方法
实例二:解决获取索引、添加、删除元素越界崩溃问题
实例三:防止按钮重复暴力点击
实例四:全局更换控件初始效果
实例五:App热修复
实例六:App异常加载占位图通用类封装
实例七:全局修改导航栏后退(返回)按钮
6.1第一个就是我们刚开始引入的时候举得例子。
6.2第二个我的Demo里面有,下有Demo下载地址
实现思路就是替换objectAtIndex:方法,在里面对索引进行判断
-(void)diyObjectAtIndex:(NSUInteger)index{
if (index < self.count) {
[self diyObjectAtIndex:index];
}else{
@try {
[self diyObjectAtIndex:index];
} @catch (NSException *exception) {
NSLog(@"数组越界");
} @finally {
}
}
}
搬运工补充知识:
这里没有使用self来调用,而是使用objc_getClass("__NSArrayM")来调用的。因为NSMutableArray的真实类只能通过后者来获取,而不能通过[self class]来获取,而method swizzling只对真实的类起作用。这里就涉及到一个小知识点:类簇。补充以上对象对应类簇表。