一、前言
1. 什么是Method Swizzling
- Method Swizzling是Objective-C的黑魔法,利用runtime实现,将两个方法的实现交换。比如:ASel -> AImp、BSel -> BImp,交换之后就是ASel -> BImp、BSel -> AImp。
2. 方法的组成
Objective-C中,方法是由SEL和IMP组成的,前者叫做方法编号,后者叫方法实现。
在OC中调用方法叫做消息发送,消息发送前会先查找消息,查找过程就是通过SEL查找IMP的过程.另外,我们经常在代码中使用@selector(oneMethod)这样的语法,@selector()叫做方法选择器,其返回的就是SEL,真正执行时会根据这个SEL查找对应的IMP。
3. Method Swizzing实现原理
- 每个类都维护一个方法Method表,Method包含
SEL和其对应IMP的信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系。 - Method Swizzing是发生在运行时的,将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后交换才起作用。
二、Method Swizzling相关函数API
// 通过SEL获取一个方法
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
// 获取一个方法的实现
method_getImplementation(Method _Nonnull m)
// 获取一个OC实现的编码类型
method_getTypeEncoding(Method _Nonnull m)
// 给方法添加实现
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
// 用一个方法的实现替换另一个方法的实现
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
// 交换两个方法的实现
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
三、Method Swizzling应用
DZPerson
@interface DZPerson : NSObject
- (void)sayHello;
- (void)fatherMethod;
@end
#import "DZPerson.h"
#import <objc/runtime.h>
@implementation DZPerson
+ (void)load {
Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
// 要交换的
Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
method_exchangeImplementations(oriMethod, swiMethod);
}
- (void)sayHello {
NSLog(@"====sayHello====");
[self sayHello];
}
- (void)fatherMethod {
NSLog(@"====fatherMethod====");
}
@end
调用
- (void)viewDidLoad {
[super viewDidLoad];
DZPerson *person = [[DZPerson alloc] init];
[person sayHello];
}
// 打印
2020-06-18 23:11:19.547721+0800 006---Method-Swizzling[11755:7044462] ====fatherMethod====
很明显,调用sayHello,实现的是fatherMethod,我们已经成功做到了方法的交换。
不知道大家有没有看到,在- (void)sayHello里边调用[self sayHello];为什么没有发生递归?
因为此时sayHello的sel找的是fatherMethod的imp,所以并不会调用sayHello方法,如下方法交换流程图:

四、Method Swizzling的使用注意点 & 坑点!!!
注意点1. 方法交换应该保证唯一性
方法交换应当在调用前完成交换,+load方法在运行时 初始化过程中类被加载的时候 调用,且每个类被加载的时候只会调用一次load方法,调用的顺序是父类、类、分类,且他们之间是相互独立的,不会存在覆盖的关系,所以放在+load方法中可以确保在使用时已经完成交换。
坑点1. 类簇的使用
调用
- (void)viewDidLoad {
[super viewDidLoad];
self.dataArray = @[@"fatherMethod",@"sayGoodBye",@"saySomething",@"sayHello"];
NSLog(@"第五个元素越界,值 = %@",[self.dataArray objectAtIndex:4]);
}
NSArray+DZTest
@interface NSArray (DZTest)
@end
#import "NSArray+DZTest.h"
#import <objc/runtime.h>
@implementation NSArray (DZTest)
+ (void)load {
Method oriMethod = class_getInstanceMethod([self class], @selector(dz_objectAtIndex:));
// 要交换的
Method swiMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:));
method_exchangeImplementations(oriMethod, swiMethod);
}
// 交换的方法
- (id)dz_objectAtIndex:(NSUInteger)index {
if (index > self.count-1) {
NSLog(@"数组越界 -- ");
return nil;
}
return [self dz_objectAtIndex:index];
}
@end
运行失败:运行会崩溃,而且加断点调试并没有进入自定义交换方法dz_objectAtIndex,崩溃信息如下,提示数组越界:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** __boundsFail: index 4 beyond bounds [0 .. 3]'
原因:
NSArray是类簇,其方法是在__NSArrayI这个类中,我们使用NSArray交换自然不会成功。解决方法:将
+load方法中的[self class]改成objc_getClass("__NSArrayI"),运行程序正常,方法交换成功,打印如下:
2020-06-19 14:55:00.360285+0800 005---Runtime应用[16329:264213] 数组越界 --
2020-06-19 14:55:00.360471+0800 005---Runtime应用[16329:264213] 第五个元素越界,值 = (null)
坑点2. 交换方法应该放到dispatch_once中执行
坑点一中,修改一下调用,先调用一次load方法:
self.dataArray = @[@"fatherMethod",@"sayGoodBye",@"saySomething",@"sayHello"];
[NSArray load];
NSLog(@"第五个元素越界,值 = %@",[self.dataArray objectAtIndex:4]);
运行失败:运行崩溃,崩溃信息还是数组越界,如下:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** __boundsFail: index 4 beyond bounds [0 .. 3]'
原因:经过调试,我们发现,在
[NSArray load];之后,会调用自定义的dz_objectAtIndex方法,但是在[self.dataArray objectAtIndex:4]时,却不再调用dz_objectAtIndex方法,这是因为手动调用了+load方法而导致方法被反复的交换,把方法又给换回去了。解决方法:所以此处应该用单例来保证方法调用的唯一性:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
// 要交换的
Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
method_exchangeImplementations(oriMethod, swiMethod);
});
坑点3. 交换子类没有实现,父类实现的方法
DZPerson
@interface DZPerson : NSObject
- (void)fatherMethod;
@end
@implementation DZPerson
- (void)fatherMethod {
NSLog(@"====fatherMethod====");
}
@end
DZStudent
#import "DZPerson.h"
@interface DZStudent : DZPerson
@end
#import "DZsStudent.h"
#import <objc/runtime.h>
@implementation DZStudent
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
// 要交换的
Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
method_exchangeImplementations(oriMethod, swiMethod);
});
}
- (void)sayHello {
[self sayHello];
NSLog(@"====sayHello====");
}
@end
调用
DZStudent *student = [[DZStudent alloc] init];
[student fatherMethod];
运行成功
2020-06-21 17:18:40.957105+0800 006---Method-Swizzling[46630:2166577] ====fatherMethod====
2020-06-21 17:18:40.957314+0800 006---Method-Swizzling[46630:2166577] ====sayHello====
但是当我们调用子类方法并交换父类方法后,父类再调用此方法,会发生崩溃,调用如下:
DZStudent *student = [[DZStudent alloc] init];
[student fatherMethod];
DZPerson *person = [[DZPerson alloc] init];
[person fatherMethod];
崩溃信息
2020-06-21 16:48:32.079486+0800 006---Method-Swizzling[42940:2112711] ====fatherMethod====
2020-06-21 16:48:32.079713+0800 006---Method-Swizzling[42940:2112711] ====sayHello====
2020-06-21 16:48:36.376793+0800 006---Method-Swizzling[42940:2112711] -[DZPerson sayHello]: unrecognized selector sent to instance 0x6000031e8e10
2020-06-21 16:48:36.387270+0800 006---Method-Swizzling[42940:2112711] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DZPerson sayHello]: unrecognized selector sent to instance 0x6000031e8e10'
原因:父类调用的时候,子类已经将此方法交换了,父类找不到交换后方法的
IMP,所以就会出现找不到方法的报错,导致程序崩溃。解决方法:交换之前先给该类添加一下需要交换的方法。
- 添加成功,说明该类没有这个方法的实现,添加后父类调用不会崩溃。
- 添加失败,说明该类已经有了这个方法的实现,父类调用也不会崩溃。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
// 要交换的
Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
// 尝试添加:class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
BOOL success = class_addMethod([self class], @selector(fatherMethod), method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) { // 自己没有 -> 交换后 -> 没有对父类进行处理 (相当于重写一个)
替换(覆盖方法,和交换方法是有区别的)
class_replaceMethod([self class], @selector(fatherMethod), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { // 自己有
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
成功打印
2020-06-21 18:11:23.895715+0800 006---Method-Swizzling[53391:2261291] ====fatherMethod====
2020-06-21 18:11:23.895999+0800 006---Method-Swizzling[53391:2261291] ====fatherMethod====
坑点4. 交换子类父类都没有实现的方法
接着上述代码,继续探究,当要交换的方法子类父类都没有实现时,即父类代码如下:
DZPerson
@interface DZPerson : NSObject
- (void)fatherMethod;
@end
@implementation DZPerson
@end
调用
LGStudent *student = [[LGStudent alloc] init];
[student fatherMethod];
会造成子类方法- (void)sayHello运行死循环。
解决方法:
我们可以在原方法也没有实现的时候给其添加一个空的实现,这样就可以防止循环调用,造成死循环。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
if (!oriMethod) {
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod([self class], @selector(fatherMethod), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"====new fatherMethod====");
}));
}
BOOL success = class_addMethod([self class], @selector(fatherMethod), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (success) { // 自己没有 -> 交换后 -> 没有对父类进行处理 (相当于重写一个)
替换(覆盖方法,和交换方法是有区别的)
class_replaceMethod([self class], @selector(fatherMethod), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { // 自己有
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
成功打印
2020-06-21 20:06:08.022162+0800 006---Method-Swizzling[69321:2526725] ====sayHello====
2020-06-21 20:06:08.022437+0800 006---Method-Swizzling[69321:2526725] ====new fatherMethod====
五、总结
Method Swizzling是Objective-C动态性的最好体现,关键在于交换两个SEL的IMP,从而达到交换方法实现的目的。

建议:
如非迫不得已,尽量少用方法交换,虽然方法交换可以让我们高效地解决问题,但是如果处理不好,可能会导致一些莫名其妙的bug,影响项目稳定性,并且很难去溯源!!!