iOS - 方法交换Method-Swizzling

了解Runtime的同学应该都听说过或者使用过Method-Swizzling,今天我们就来一起了解下Method-Swizzling的使用以及坑点。

一、Method-Swizzling的使用

新建工程,创建一个工程,创建一个LPPerson继承于NSObject,和继承于LPPersonLPMan类,再创建一个LPMan的类别,和一个runtime的工具类:
LPPerson类:

@interface LPPerson : NSObject
- (void)personInstanceMetheod;
+ (void)personClassMetheod;
@end

@implementation LPPerson
- (void)personInstanceMetheod{
    NSLog(@"%s",__func__);
}
+ (void)personClassMetheod{
    NSLog(@"%s",__func__);
}
@end

LPMan类:

/// LPPerson的子类
@interface LPMan : LPPerson

@end

@implementation LPMan

@end

LPMan的类别:

////类别
@interface LPMan (MS)

@end

@implementation LPMan (MS)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        [LPRunTimeTool lp_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMetheod) swizzledSEL:@selector(lp_manInstanceMethod)];
}

- (void)lp_manInstanceMethod{
    [self lp_manInstanceMethod]; //lg_studentInstanceMethod -/-> personInstanceMethod
    NSLog(@"LPMan分类添加的lg对象方法:%s",__func__);
}
@end

工具类:

@interface LPRunTimeTool : NSObject
/**
 交换方法
 @param cls 交换对象
 @param oriSEL 原始方法编号
 @param swizzledSEL 交换的方法编号
 */
+ (void)lp_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;

@end

#import <objc/runtime.h>
@implementation LPRunTimeTool
+ (void)lp_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}

@end

ok,接下来,我们在Viewcontroller中完成调用:

- (void)viewDidLoad {
    [super viewDidLoad];
    // 黑魔法坑点二: 子类没有实现 - 父类实现
    LPMan *s = [[LPMan alloc] init];
    [s personInstanceMetheod];
}

运行查看结果:

2020-10-25 14:51:17.067194+0800 Method-SwizzlingTest[55258:1422381] -[LPPerson personInstanceMetheod]
2020-10-25 14:51:17.067288+0800 Method-SwizzlingTest[55258:1422381] LPMan分类添加的lg对象方法:-[LPMan(MS) lp_manInstanceMethod]

结果是正确的,我们来分析下为什么是正确的:

  • 1:结合我们前面学习的知识,知道dyld链接过程中,会先调用LPMan类别的load方法,load方法中执行LPRunTimeToollp_methodSwizzlingWithClass:self方法
  • 2:lp_methodSwizzlingWithClass:self里面调用了系统的method_exchangeImplementations方法,已经将LPPersonpersonInstanceMetheodimpLPManlp_manInstanceMethodimp实现了交换
  • 3:LPMan的对象,调用父类的personInstanceMetheod方法时,实际是执行的LPManlp_manInstanceMethod方法,
  • 4:lp_manInstanceMethod执行时,先自己调用了一次lp_manInstanceMethod,此时实际上是执行的LPPersonpersonInstanceMetheod方法,所以会打印第一次
  • 5:然后再继续执行lp_manInstanceMethod里面,所以会打印第二次

二、坑点

坑点1:

我们在Viewcontroller中创建一个LPPerson,并且去调用他自己的personInstanceMetheod:

- (void)viewDidLoad {
    [super viewDidLoad];
    LPMan *s = [[LPMan alloc] init];
    [s personInstanceMetheod];

    LPPerson *p = [[LPPerson alloc] init];
    [p personInstanceMetheod];
}

运行查看结果:

2020-10-25 15:09:45.518448+0800 Method-SwizzlingTest[55464:1432664] -[LPPerson personInstanceMetheod]
2020-10-25 15:09:45.518581+0800 Method-SwizzlingTest[55464:1432664] LPMan分类添加的lg对象方法:-[LPMan(MS) lp_manInstanceMethod]
2020-10-25 15:09:45.518656+0800 Method-SwizzlingTest[55464:1432664] -[LPPerson lp_manInstanceMethod]: unrecognized selector sent to instance 0x600000004520
2020-10-25 15:09:45.518880+0800 Method-SwizzlingTest[55464:1432664] Failed to set (contentViewController) user defined inspected property on (NSWindow): -[LPPerson lp_manInstanceMethod]: unrecognized selector sent to instance 0x600000004520

可以看到,s调用personInstanceMetheod的时候,没有问题,但是p调用personInstanceMetheod的时候就崩溃了,这是因为什么呢?
LPMan的对象调用personInstanceMetheod的时候,内部已经完成了方法交换,所以这一步是不会有问题的,但是LPPerson的对调用personInstanceMetheod的时候,因为已经完成了方法交换,实际上是调用的LPManlp_manInstanceMethod方法,但是对于LPPerson本身来说,它并没有这个方法,这个时候LPPerson就会沿着它的继承链一直到找到nil,但是都没有,所以就会直接崩溃。

那么我们怎么解决呢?
既然报错是因为LPPerson没有这个方法,那如果它有了不就可以了吗?所以我们就手动给它加上,在LPRuntimeTool中添加方法:

+ (void)lp_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;
+ (void)lp_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    // oriSEL       personInstanceMethod
    // swizzledSEL  lg_studentInstanceMethod
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // 尝试添加你要交换的方法 - lg_studentInstanceMethod
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

    
    if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // 自己有
        method_exchangeImplementations(oriMethod, swiMethod);
    }
  
}

然后在LPMan的分类中调用:

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LPRunTimeTool lp_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMetheod) swizzledSEL:@selector(lp_manInstanceMethod)];
    });
}

运行,查看结果:

2020-10-25 15:23:46.527062+0800 Method-SwizzlingTest[55557:1439736] -[LPPerson personInstanceMetheod]
2020-10-25 15:23:46.527158+0800 Method-SwizzlingTest[55557:1439736] LPMan分类添加的lg对象方法:-[LPMan(MS) lp_manInstanceMethod]
2020-10-25 15:23:46.527203+0800 Method-SwizzlingTest[55557:1439736] -[LPPerson personInstanceMetheod]

dangdang,完美。我们再分析下lp_betterMethodSwizzlingWithClass:方法:

  • 1.首先判断当前类,如果类都不存在,后面完全没有必要进行了,所以直接return
  • 2.给当前类是添加原始sel,但是用的是交换的selimp,这一步主要是当前类是否存在原始的sel,如果自己有,则添加不成功返回NO,如果自己没有,但是父类有,或者其继承链的的其他类有,则添加成功,返回YES。根据我们前面学到的知识,可以知道,子类重写父类的方法,在方法列表中会放在最前面。
  • 3.判断添加的结果:
    • 1).如果添加成功:说明之前类没有实现,所以我们直接使用class_replaceMethod,使用需要原始方法的imp替换了需要交换方法的imp。即oriSELswiMethodimpswizzledSELoriMethodimp这样也就完成了方法交换。此时,LPPerson调用personInstanceMetheod,还是personInstanceMetheodimp,所以不会报错。
    • 2).如果添加不成功,说明自己本身有了,所以直接交换即可。此时,LPPerson调用personInstanceMetheod就是我们之前分析的过程,也不会报错。

坑点2:

接下来,我们将LPPersonpersonInstanceMetheod的实现注释掉,以及ViewcontrollerLPPerson的代码也注释掉:

@implementation LPPerson
//- (void)personInstanceMetheod{
//    NSLog(@"%s",__func__);
//}
+ (void)personClassMetheod{
    NSLog(@"%s",__func__);
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // 黑魔法坑点二: 子类没有实现 - 父类实现
    LPMan *s = [[LPMan alloc] init];
    [s personInstanceMetheod];

    // personInstanceMethod -> lg_studentInstanceMethod
//    LPPerson *p = [[LPPerson alloc] init];
//    [p personInstanceMetheod];
}

再次运行看下结果:


image.png

可以看到,这个时候在lp_manInstanceMethod方法发生了死循环,导致崩溃了,但是为什么会造成死循环呢?
这是因为在方法交换的时候,因为LPPerson中没有实现personInstanceMetheod,所以获取到的 oriMethod就是nil,这时候去交换方法,结果就是lp_manInstanceMethodimp并没有交换给personInstanceMetheod,所以在lp_manInstanceMethod中再调用lp_manInstanceMethod,实际就是递归了,所以就发生了死循环。

image.png

既然是因为oriMethod没有,那我们就判断下,如果没有就给它添加上不就行了吗?在LPRunTimeTool中继续添加如下方法,并在LPMan中调用:

+ (void)lp_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    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(@"来了一个空的 imp");
        }));
    }
    
    //   一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    //   交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

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

再次运行:

2020-10-25 16:08:42.852133+0800 Method-SwizzlingTest[56249:1537593] 来了一个空的 imp
2020-10-25 16:08:42.852205+0800 Method-SwizzlingTest[56249:1537593] LPMan分类添加的lg对象方法:-[LPMan(MS) lp_manInstanceMethod]

可以看到,死循环的问题,已经解决了。
我们来分析下lp_bestMethodSwizzlingWithClass中代码:对比lp_betterMethodSwizzlingWithClass:主要就是增加对oriMethod的判断,如果为空,我们则手动添加一个方法,并且通过method_setImplementation重设它的imp。在这里,你可以完成很多的事情,比如说错误上报啊等等。

三、类方法的交换

其实通过实例方法的交换以及我们前面知道继承链关系,类方法的交换和实例方法的交换非常类似,唯一的区别是类方法存在元类中。所以,同样的,我们也可以构造一个类方法的交换方法:

+ (void)lp_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getClassMethod([cls class], oriSEL);
    Method swiMethod = class_getClassMethod([cls class], swizzledSEL);
    
    if (!oriMethod) { // 避免动作没有意义
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"来了一个空的 imp");
        }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

    BOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    if (didAddMethod) {
        class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
}

在我们实际开发中,利用method-swizzling可以做非常多的事情,比如崩溃拦截等等,感兴趣的同学可以自己去探索哦!

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