前言
关于Method-swizzling,对于iOS开发者来讲并不陌生,且应用自如。今天我们主要整理一下关于Method-swizzling使用时的一些注意事项,从而避免采坑。
开始
准备工作:创建ZZPerson类,创建ZZStudent类(继承自ZZPerson),创建ZZStudent+Swizzling.h分类
ZZPerson
@interface ZZPerson : NSObject
- (void)toDoSomething;
@end
@implementation ZZPerson
- (void)toDoSomething
{
NSLog(@" %s",__func__);
}
@end
ZZStudent
@interface ZZStudent : ZZPerson
@end
@implementation ZZStudent
@end
ZZStudent+Swizzling
@interface ZZStudent (Swizzling)
@end
@implementation ZZStudent (Swizzling)
+ (void)load
{
}
+ (void)initialize
{
}
@end
- 注意事项一:
Method-swizzling触发的时机问题
从ZZStudent+Swizzling可以看到我在这里实现了+(void) load和+ (void)initialize两个方法。首先说下两者的区别:
+(void) load:这个方法会在应用程序启动时,main()函数前执行。(即dyld:_start()->_ojbc_init()->_dyld_objc_notify_register()->notifySingle()->load_images()),这也就意味着更多的load方法会影响整个应用的启动。
+ (void)initialize:这个方法的执行时机是当类第一次发送消息(objc_msgSend()),也可以理解为第一次使用这个类的时候执行。显然这里进行方法交换更合理一点。
在方法交换时我们要使用GCD dispath_once_t包裹一下,来避免重复执行。
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//TODO method-swizzling
});
}
- 注意事项二:子类交换父类方法
这里我们在+initialize方法内举例,添加方法交换代码:
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//TODO method-swizzling
[self zz_methodSwizzlingWithClass0:self oriSEL:@selector(toDoSomething) swizzledSEL:@selector(customDoSomething)];
});
}
- (void)customDoSomething
{
[self customDoSomething];
NSLog(@"方法来了");
}
+ (void)zz_methodSwizzlingWithClass0:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL
{
if(!cls) return;
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
以上代码我们交换的toDoSomething和customDoSomething,而这里要注意的是toDoSomething方法是在父类中,这里将父类的一个IMP跟子类的IMP做了交换。
调用一下看是否交换成功
- (void)viewDidLoad {
[super viewDidLoad];
ZZStudent *student = [ZZStudent alloc];
[student toDoSomething];
}
输出结果:
2020-10-29 13:58:17.189210+0800 TestMemoryShift[1519:975259] -[ZZPerson toDoSomething]
2020-10-29 13:58:17.189432+0800 TestMemoryShift[1519:975259] 方法来了
这里通过toDoSomething成功调到了customDoSomething,貌似没什么问题。
接下来我们让ZZPerson调用下toDoSomething,看会发生什么
- (void)viewDidLoad {
[super viewDidLoad];
ZZStudent *student = [ZZStudent alloc];
[student toDoSomething];
ZZPerson *person = [ZZPerson alloc];
[person toDoSomething];
}
-[ZZPerson customDoSomething]: unrecognized selector sent to instance 0x2815e46a0
2020-10-29 14:00:58.528901+0800 TestMemoryShift[1522:976000] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ZZPerson customDoSomething]: unrecognized selector sent to instance 0x2815e46a0'
此时程序崩溃,并抛出unrecognized selector sent to instance xxx错误信息,这也就说明ZZPerson内找不到toDoSomething的实现。这是为什么呐?ZZPerson内明明是有toDoSomething的实现呀?
解释:首先方法交换后,toDoSomething的调用也就是找customDoSomething,而此时
stuent->customDoSomething是可以找到方法的,而person->customDoSomething,此时的ZZPerson下并没有找到customDoSomething的实现,而且父类是不会向下查询IMP的,而是向ZZPerson的父类继续查询,最终发生了崩溃。
这里我们就需要做一些处理,来规避这样的坑了。为了避免影响父类的SEL指向,我们在给子类交换方法时,先尝试给子类添加一个toDoSomething方法,并将实现指向(IMP) customDoSomething,添加成功后,将customDoSomething的实现替换为(IMP)toDoSomething。
+ (void)zz_methodSwizzlingWithClass1:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL
{
if(!cls) return;
//oriSEL->oriMethod
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
//swizzledSEL->swiMethod
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
//尝试添加 oriSEL->swiMethod
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if(success){
//添加成功 替换 swizzledSEL->oriMethod
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
//存在 直接交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
再次运行代码,就不会崩溃了。这里就相当于我们动态的给子类添加了toDoSomething的方法,并将实现指向了(IMP)customDoSomething,这里并没有修改父类ZZPerson中toDoSomething的指向,所以父类不受影响。
[1546:994057] receiver:<ZZStudent: 0x2801506e0> -[ZZPerson toDoSomething]
[1546:994057] receiver:<ZZStudent: 0x2801506e0>_-[ZZStudent(Swizzling) customDoSomething]
[1546:994057] receiver:<ZZPerson: 0x2801506f0> -[ZZPerson toDoSomething]
这里还有一个坑点:当父类的toDoSomething的实现某天不存在了,那此时的class_replaceMethod或者method_exchangeImplementations将会失败,我们注释掉父类的toDoSomething运行代码:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x16f60ffe0)
此时发生了递归循环,内存溢出,导致崩溃

这里在方法交换时
(SEL)toDoSomething成功指向了(IMP)customDoSomething,但(IMP)toDoSomething的实现并不存在,导致(SEL)customDoSomething指向失败,所以还是指向(IMP)customDoSomething,所以发生了递归循环。这种情况该如何避免呐???????
这里我们就需要在完善的一个点就是:判断原始的方法是否存在实现,如果不存在,动态添加实现。
+ (void)zz_methodSwizzlingWithClass2:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) return;
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"没有找到对应的实现");
}));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
此时执行程序
[1563:999857] <ZZStudent: 0x282cb0710>-(null) 没有找到对应的实现
[1563:999857] receiver:<ZZStudent: 0x282cb0710>_-[ZZStudent(Swizzling) customDoSomething]
总结
以上是开发中遇到的一些关于method-swizzling使用中遇到的一些问题,这里总结整理下,以备查阅。