iOS-OC底层Method-swizzling的那些坑

前言

关于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);
}

以上代码我们交换的toDoSomethingcustomDoSomething,而这里要注意的是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,这里并没有修改父类ZZPersontoDoSomething的指向,所以父类不受影响。

[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)

此时发生了递归循环,内存溢出,导致崩溃

截屏2020-10-29 下午3.46.12.png

这里在方法交换时(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使用中遇到的一些问题,这里总结整理下,以备查阅。

你可能感兴趣的:(iOS-OC底层Method-swizzling的那些坑)