Runtime — Method Swizzling黑魔法

一、前言

1. 什么是Method Swizzling

  1. Method Swizzling是Objective-C的黑魔法,利用runtime实现,将两个方法的实现交换。比如:ASel -> AImpBSel -> BImp,交换之后就是ASel -> BImpBSel -> AImp

2. 方法的组成

Objective-C中,方法是由SELIMP组成的,前者叫做方法编号,后者叫方法实现。
在OC中调用方法叫做消息发送,消息发送前会先查找消息,查找过程就是通过SEL查找IMP的过程.另外,我们经常在代码中使用@selector(oneMethod)这样的语法,@selector()叫做方法选择器,其返回的就是SEL,真正执行时会根据这个SEL查找对应的IMP

3. Method Swizzing实现原理

  • 每个类都维护一个方法Method表,Method包含SEL和其对应IMP的信息,方法交换做的事情就是把SELIMP的对应关系断开,并和新的IMP生成对应关系。
  • Method Swizzing是发生在运行时的,将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后交换才起作用。

二、Method Swizzling相关函数API

// 通过SEL获取一个方法
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
// 获取一个方法的实现
method_getImplementation(Method _Nonnull m)
// 获取一个OC实现的编码类型
method_getTypeEncoding(Method _Nonnull m)
// 给方法添加实现
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 
// 用一个方法的实现替换另一个方法的实现
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
// 交换两个方法的实现
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)

三、Method Swizzling应用

DZPerson

@interface DZPerson : NSObject
- (void)sayHello;
- (void)fatherMethod;
@end

#import "DZPerson.h"
#import <objc/runtime.h>
@implementation DZPerson
+ (void)load {
    Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
    // 要交换的
    Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
    method_exchangeImplementations(oriMethod, swiMethod);
}

- (void)sayHello {
    NSLog(@"====sayHello====");
    [self sayHello];
}

- (void)fatherMethod {
    NSLog(@"====fatherMethod====");
}

@end

调用

- (void)viewDidLoad {
    [super viewDidLoad];
    DZPerson *person = [[DZPerson alloc] init];
    [person sayHello];
}

// 打印
2020-06-18 23:11:19.547721+0800 006---Method-Swizzling[11755:7044462] ====fatherMethod====

很明显,调用sayHello,实现的是fatherMethod,我们已经成功做到了方法的交换。

不知道大家有没有看到,在- (void)sayHello里边调用[self sayHello];为什么没有发生递归?
因为此时sayHellosel找的是fatherMethodimp,所以并不会调用sayHello方法,如下方法交换流程图:

四、Method Swizzling的使用注意点 & 坑点!!!

注意点1. 方法交换应该保证唯一性

方法交换应当在调用前完成交换,+load方法在运行时 初始化过程中类被加载的时候 调用,且每个类被加载的时候只会调用一次load方法,调用的顺序是父类分类,且他们之间是相互独立的,不会存在覆盖的关系,所以放在+load方法中可以确保在使用时已经完成交换。

坑点1. 类簇的使用

调用

- (void)viewDidLoad {
    [super viewDidLoad];    
    self.dataArray = @[@"fatherMethod",@"sayGoodBye",@"saySomething",@"sayHello"];    
    NSLog(@"第五个元素越界,值 = %@",[self.dataArray objectAtIndex:4]);    
}

NSArray+DZTest

@interface NSArray (DZTest)
@end

#import "NSArray+DZTest.h"
#import <objc/runtime.h>
@implementation NSArray (DZTest)
+ (void)load {
    Method oriMethod = class_getInstanceMethod([self class], @selector(dz_objectAtIndex:));
    // 要交换的
    Method swiMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:));
    method_exchangeImplementations(oriMethod, swiMethod);
}
// 交换的方法
- (id)dz_objectAtIndex:(NSUInteger)index {
    if (index > self.count-1) {
        NSLog(@"数组越界 -- ");
        return nil;
    }
    return [self dz_objectAtIndex:index];
}
@end

运行失败:运行会崩溃,而且加断点调试并没有进入自定义交换方法dz_objectAtIndex,崩溃信息如下,提示数组越界:

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** __boundsFail: index 4 beyond bounds [0 .. 3]'

原因:NSArray是类簇,其方法是在__NSArrayI这个类中,我们使用NSArray交换自然不会成功。

解决方法:+load方法中的[self class]改成objc_getClass("__NSArrayI"),运行程序正常,方法交换成功,打印如下:

2020-06-19 14:55:00.360285+0800 005---Runtime应用[16329:264213] 数组越界 -- 
2020-06-19 14:55:00.360471+0800 005---Runtime应用[16329:264213] 第五个元素越界,值 = (null)

坑点2. 交换方法应该放到dispatch_once中执行

坑点一中,修改一下调用,先调用一次load方法:

self.dataArray = @[@"fatherMethod",@"sayGoodBye",@"saySomething",@"sayHello"];
[NSArray load];
NSLog(@"第五个元素越界,值 = %@",[self.dataArray objectAtIndex:4]);

运行失败:运行崩溃,崩溃信息还是数组越界,如下:

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** __boundsFail: index 4 beyond bounds [0 .. 3]'

原因:经过调试,我们发现,在[NSArray load];之后,会调用自定义的dz_objectAtIndex方法,但是在[self.dataArray objectAtIndex:4]时,却不再调用dz_objectAtIndex方法,这是因为手动调用了+load方法而导致方法被反复的交换,把方法又给换回去了。

解决方法:所以此处应该用单例来保证方法调用的唯一性:

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
    // 要交换的
    Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
    method_exchangeImplementations(oriMethod, swiMethod);
});

坑点3. 交换子类没有实现,父类实现的方法

DZPerson

@interface DZPerson : NSObject
- (void)fatherMethod;
@end

@implementation DZPerson
- (void)fatherMethod {
    NSLog(@"====fatherMethod====");
}
@end

DZStudent

#import "DZPerson.h"
@interface DZStudent : DZPerson
@end

#import "DZsStudent.h"
#import <objc/runtime.h>
@implementation DZStudent
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
        // 要交换的
        Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
        method_exchangeImplementations(oriMethod, swiMethod);
    });
}
 
- (void)sayHello {
    [self sayHello];
    NSLog(@"====sayHello====");
}
@end

调用

DZStudent *student = [[DZStudent alloc] init];
[student fatherMethod];

运行成功

2020-06-21 17:18:40.957105+0800 006---Method-Swizzling[46630:2166577] ====fatherMethod====
2020-06-21 17:18:40.957314+0800 006---Method-Swizzling[46630:2166577] ====sayHello====

但是当我们调用子类方法并交换父类方法后,父类再调用此方法,会发生崩溃,调用如下:

DZStudent *student = [[DZStudent alloc] init];
[student fatherMethod];

DZPerson *person = [[DZPerson alloc] init];
[person fatherMethod];

崩溃信息

2020-06-21 16:48:32.079486+0800 006---Method-Swizzling[42940:2112711] ====fatherMethod====
2020-06-21 16:48:32.079713+0800 006---Method-Swizzling[42940:2112711] ====sayHello====
2020-06-21 16:48:36.376793+0800 006---Method-Swizzling[42940:2112711] -[DZPerson sayHello]: unrecognized selector sent to instance 0x6000031e8e10
2020-06-21 16:48:36.387270+0800 006---Method-Swizzling[42940:2112711] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DZPerson sayHello]: unrecognized selector sent to instance 0x6000031e8e10'

原因:父类调用的时候,子类已经将此方法交换了,父类找不到交换后方法的IMP,所以就会出现找不到方法的报错,导致程序崩溃。

解决方法:交换之前先给该类添加一下需要交换的方法。

  • 添加成功,说明该类没有这个方法的实现,添加后父类调用不会崩溃。
  • 添加失败,说明该类已经有了这个方法的实现,父类调用也不会崩溃。
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
        // 要交换的
        Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
        
        // 尝试添加:class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
        BOOL success = class_addMethod([self class], @selector(fatherMethod), method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

        if (success) { // 自己没有 -> 交换后 -> 没有对父类进行处理 (相当于重写一个)
            替换(覆盖方法,和交换方法是有区别的)
            class_replaceMethod([self class], @selector(fatherMethod), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else { // 自己有
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}

成功打印

2020-06-21 18:11:23.895715+0800 006---Method-Swizzling[53391:2261291] ====fatherMethod====
2020-06-21 18:11:23.895999+0800 006---Method-Swizzling[53391:2261291] ====fatherMethod====

坑点4. 交换子类父类都没有实现的方法

接着上述代码,继续探究,当要交换的方法子类父类都没有实现时,即父类代码如下:
DZPerson

@interface DZPerson : NSObject
- (void)fatherMethod;
@end

@implementation DZPerson

@end

调用

LGStudent *student = [[LGStudent alloc] init];
[student fatherMethod];

会造成子类方法- (void)sayHello运行死循环。

解决方法:
我们可以在原方法也没有实现的时候给其添加一个空的实现,这样就可以防止循环调用,造成死循环。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Method oriMethod = class_getInstanceMethod([self class], @selector(fatherMethod));
        Method swiMethod = class_getInstanceMethod([self class], @selector(sayHello));
        
        if (!oriMethod) {
            // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
            class_addMethod([self class], @selector(fatherMethod), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
                NSLog(@"====new fatherMethod====");
            }));
        }
        
        BOOL success = class_addMethod([self class], @selector(fatherMethod), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

        if (success) { // 自己没有 -> 交换后 -> 没有对父类进行处理 (相当于重写一个)
            替换(覆盖方法,和交换方法是有区别的)
            class_replaceMethod([self class], @selector(fatherMethod), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else { // 自己有
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });   
}

成功打印

2020-06-21 20:06:08.022162+0800 006---Method-Swizzling[69321:2526725] ====sayHello====
2020-06-21 20:06:08.022437+0800 006---Method-Swizzling[69321:2526725] ====new fatherMethod====

五、总结

Method SwizzlingObjective-C动态性的最好体现,关键在于交换两个SELIMP,从而达到交换方法实现的目的。

建议:
如非迫不得已,尽量少用方法交换,虽然方法交换可以让我们高效地解决问题,但是如果处理不好,可能会导致一些莫名其妙的bug,影响项目稳定性,并且很难去溯源!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容