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

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

截屏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] <ZZStudent: 0x282cb0710>-(null) 没有找到对应的实现
[1563:999857] receiver:<ZZStudent: 0x282cb0710>_-[ZZStudent(Swizzling) customDoSomething]
总结

以上是开发中遇到的一些关于method-swizzling使用中遇到的一些问题,这里总结整理下,以备查阅。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336