iOS底层-Method Swizzling

1,138 阅读2分钟

前言

  • 作为一个iOS开发,肯定或多或少的听到过iOS黑魔法,也就是方法交换,但这个过程是怎样的,原理又是怎样的,有什么应用场景呢?本文将一一探究这些问题

简介

  • Method Swizzling我们通常称之为黑魔法,通俗的来讲就是方法交换
  • 每个类都有一个方法列表,列表中每个方法的SELIMP是对应关系,方法交换就是使SEL对应新的IMP,如下图所示:

截屏2021-07-31 18.04.03.png

案例分析

方法交换

创建一个类WSPerson,然后创建WSTeacher继承WSPerson:

// WSPerson .h
@interface WSPerson : NSObject
- (void)person_instanceMethod;

@end

// WSPerson.m
@implementation WSPerson

- (void)person_instanceMethod {
    NSLog(@"\n🎈print person_instanceMethod: %s\n", __func__);
}

@end

// WSTeacher.h
@implementation WSTeacher

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [WSRuntimeUtil ws_methodSwizzlingWithClass:self
                                            oriSEL:@selector(person_instanceMethod)
                                       swizzledSEL:@selector(teacher_instanceMethod)];
    });
}

- (void)teacher_instanceMethod {
    NSLog(@"\n🎉 print teacher_instanceMethod: %s\n", __func__);
}

@end

// WSRuntimeUtil.m
+ (void)ws_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swizzleMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swizzleMethod);
}

然后两个类创建对象分别去调用person_instanceMethod方法:

截屏2021-07-31 22.52.23.png
运行结果都打印了teacher_instanceMethod方法

  • 因为方法交换后person_instanceMethod指向了teacher_instanceMethodIMP,进而找到了teacher_instanceMethod的方法;
  • teacher_instanceMethod指向了person_instanceMethodIMP,但IMP对应的是person_instanceMethod方法,再继续根据person_instanceMethod方法去找IMP,就找到了交换后的IMP,进而找到了teacher_instanceMethod方法
  • 过程如下: 截屏2021-07-31 23.36.36.png

递归问题

teacher_instanceMethod方法稍作修改:

- (void)teacher_instanceMethod {
    [self teacher_instanceMethod];
    NSLog(@"\n🎉 print teacher_instanceMethod: %s\n", __func__);
}

再运行,结果如下:

截屏2021-08-01 00.13.43.png
结果出现[WSPerson teacher_instanceMethod],所以就出现异常找不到方法

  • [person person_instanceMethod]时,由于方法交换,person_instanceMethod找到了teacher_instanceMethodIMP,由于消息的接受者是person,所以此时[self teacher_instanceMethod]相当于[person teacher_instanceMethod],因为WSPerson类没有teacher_instanceMethod方法,所以就出现异常了
  • 为什么要调用自己呢,是因为有时候做一些处理的时候,需要保持原来的逻辑,所以需要再次调用自己,那怎样才能避免这种异常呢?可以通过class_addMethod去尝试添加要交换的方法

优化一

  • 使用class_addMethod去尝试添加要交换的方法,如果添加成功,即类中没有这个方法,则通过class_replaceMethod进行替换,其内部会调用class_addMethod进行添加
  • 如果添加不成功,即类中有这个方法,则通过method_exchangeImplementations进行交换
  • 优化代码如下:
+ (void)ws_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // 尝试添加你要交换的方法
    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);
    }
}

优化二

  • 如果子类和父类都没有实现person_instanceMethod方法,调用[self teacher_instanceMethod]时就会产生递归,如果方法不存在,可以在添加方法后给她添加一个空的实现,也就是增加一个不做任何事情的IMP

截屏2021-08-01 00.59.07.png

  • 这样就避免了因都未实现方法而导致的递归