前言
关于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: -[ZZPerson toDoSomething]
[1546:994057] receiver:_-[ZZStudent(Swizzling) customDoSomething]
[1546:994057] receiver: -[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] -(null) 没有找到对应的实现
[1563:999857] receiver:_-[ZZStudent(Swizzling) customDoSomething]
总结
以上是开发中遇到的一些关于method-swizzling使用中遇到的一些问题,这里总结整理下,以备查阅。