19.MethodSwizzing方法交换的坑

前言

MethodSwizzing 方法交换是比较常用的所谓 黑魔法。但正如武侠小说中的绝世武功一般,也存在使用不恰当发生 伤敌一千,自损八百 的情况。

本文就带你来探索一下其中的坑,避免走火入魔。

一、 MethodSwizzingTool

为了方便,我们封装一下常规的方法交换逻辑

@implementation MethodSwizzingTool

+ (void)swizzingClass:(Class)cls oldSEL:(SEL)oldSel toNewSel:(SEL)newSel {
    if (!cls) { return; }
    Method oldM = class_getInstanceMethod(cls, oldSel);
    Method newM = class_getInstanceMethod(cls, newSel);
    method_exchangeImplementations(oldM, newM);
}

@end

1.1 验证是否有效

@implementation RYModel

+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [MethodSwizzingTool swizzingClass:self oldSEL:@selector(functionA) toNewSel:@selector(functionB)];
    });
}

- (void)functionA {
    NSLog(@"%s", __func__);
}

- (void)functionB {
    NSLog(@"%s", __func__);
    [self functionB];
}

@end

调用:

RYModel *ry = [[RYModel alloc] init];
[ry functionA];

输出:

2021-08-07 17:01:53.954201+0800 MethodSwizzing[72309:15057493] -[RYModel functionB]
2021-08-07 17:01:53.954534+0800 MethodSwizzing[72309:15057493] -[RYModel functionA]

嗯~感觉封装的没问题。

二、 子类的坑

2.1 用自类方法替换父类方法,会怎样?

思考:

在子类 RYSubModel 中用 子类subFunctionA 替换 父类functionA子类实例父类实例分别调用 functionA 会是什么样的结果呢?(父类中未做交换)

子类代码如下:

@implementation RYSubModel

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [MethodSwizzingTool swizzingClass:self oldSEL:@selector(functionA) toNewSel:@selector(subFunctionA)];
    });
}

- (void)subFunctionA {
    NSLog(@"%s", __func__);
}

@end

调用:

RYModel *ry = [[RYModel alloc] init];
[ry functionA];

RYSubModel *sub = [[RYSubModel alloc] init];
[sub functionA];

输出:

2021-08-07 17:10:37.990097+0800 MethodSwizzing[72705:15063434] -[RYSubModel subFunctionA]
2021-08-07 17:10:37.990530+0800 MethodSwizzing[72705:15063434] -[RYSubModel subFunctionA]

分析

用子类中的方法 subFunctionA 替换父类中的方法 functionAfunctionA 的实现变成了 subFunctionA

2.2 调用原实现

一般我们交换方法后想要继续调用原本的实现一般会如上文中functionB那样调用一下自己。

那么我们在用子类中的方法 subFunctionA 替换了父类中的方法 functionA 后想要继续调用 functionA 同理应该这么写:

- (void)subFunctionA {
    [self subFunctionA];
    NSLog(@"%s", __func__);
}

调用:

RYModel *ry = [[RYModel alloc] init];
[ry functionA];

RYSubModel *sub = [[RYSubModel alloc] init];
[sub functionA];

输出:

为什么呢?居然找不到了

分析

我们在用子类中的方法 subFunctionA 替换了父类中的方法 functionA

父类中:

functionA 调用 subFunctionA

但是父类本身方法列表中并没有 subFunctionA ,所以父类就报了 unrecognized selector 的错误。

修改调用

如果我们只调用子类:

RYSubModel *sub = [[RYSubModel alloc] init];
[sub functionA];

输出:

2021-08-07 17:33:28.874108+0800 MethodSwizzing[73690:15075457] -[RYModel functionA]
2021-08-07 17:33:28.874564+0800 MethodSwizzing[73690:15075457] -[RYSubModel subFunctionA]

这里就是正常的

三、 优化 MethodSwizzingTool

我们能不能优化 MethodSwizzingTool 来防止这样的问题出现呢?

可以!

3.1 优化思路

  • 出现上面找不到方法的原因是:子类用自己的实现直接替换了父类的方法

  • 那么我们能不能为子类动态添加一个和父类一样的方法呢?子类中进行替换的时候就不会影响父类了

3.2 编写优化代码

+ (void)swizzingClassB:(Class)cls oldSEL:(SEL)oldSel toNewSel:(SEL)newSel {
    if (!cls) { return; }
    Method oldM = class_getInstanceMethod(cls, oldSel);
    Method newM = class_getInstanceMethod(cls, newSel);

    // 先尝试给 cls 添加方法(SEL: oldSel  IMP: newM),防止子类直接替换父类中的方法
    BOOL addSuccess = class_addMethod(cls, oldSel, method_getImplementation(newM), method_getTypeEncoding(oldM));

    if (addSuccess) { // 添加成功即:原本没有 oldSel,成功为子类添加了一个 oldSel - newM 的方法
        // 这里将原 newSel的imp替换为 oldM 的 IMP
        class_replaceMethod(cls, newSel, method_getImplementation(oldM), method_getTypeEncoding(oldM));
    }
    else {
        method_exchangeImplementations(oldM, newM);
    }
}

3.3 优化代码流程分析

本案例子类调用流程分析:

  • 调用方法,准备用子类中的方法 subFunctionA 替换的方法 functionA

  • Method oldM 是从父类获取到的方法 (SEL: functionA, IMP: functionA)

  • Method newM 是从子类自己获取到的方法 (SEL: subFunctionA, IMP: subFunctionA)

  • 是否可以成功添加方法:(SEL: functionA, IMP: subFunctionA)

    • 否:已存在 SEL: functionA 的方法

    • 是:不存在 SEL: functionA 的方法

      • 将子类的 (SEL: subFunctionA, IMP: subFunctionA) 替换为 (SEL: subFunctionA, IMP: functionA)

  • 父类实例

    • [ry functionA]

    • 调用没有受子类方法交换的影响

  • 子类实例

    • [sub functionA]

    • 没有出现父类调用找不到方法的情况

四、 父类的 functionA 也没有实现呢?

4.1 分析

由于并不存在 functionA 的实现,所以这里的替换方法并没有成功。 subFunctionA 的调用就直接递归死循环了。

4.2 再优化

通过设置默认实现的方式来避免死循环,新实现也为空的情景可以类似处理,这里就不赘述了。

+ (void)swizzingClassB:(Class)cls oldSEL:(SEL)oldSel toNewSel:(SEL)newSel {
    if (!cls) { return; }
    Method oldM = class_getInstanceMethod(cls, oldSel);
    Method newM = class_getInstanceMethod(cls, newSel);

    if (!oldM) {
        // 先用新的实现来,临时添加一个(这里忽略新实现也为空的情况,可以类似的处理)
        class_addMethod(cls, oldSel, method_getImplementation(newM), method_getTypeEncoding(newM));
        // 对 oldM 变量重新赋值
        oldM = class_getInstanceMethod(cls, oldSel);
        // 创建默认实现,可以进行一些日志收集之类的
        IMP defaultIMP = imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"一些处理");
        });
        // 为 oldM 设置 IMP
        method_setImplementation(oldM, defaultIMP);
    }

    // 先尝试给 cls 添加方法(SEL: oldSel  IMP: newM),防止子类直接替换父类中的方法
    BOOL addSuccess = class_addMethod(cls, oldSel, method_getImplementation(newM), method_getTypeEncoding(oldM));

    if (addSuccess) { // 添加成功即:原本没有 oldSel,成功为子类添加了一个 oldSel - newM 的方法
        // 这里将原 newSel的imp替换为 oldM 的 IMP
        class_replaceMethod(cls, newSel, method_getImplementation(oldM), method_getTypeEncoding(oldM));
    }
    else {
        method_exchangeImplementations(oldM, newM);
    }
}

总结

黑魔法虽好,使用也得倍加小心!

尤其要对方法的本质以及方法调用的流程烂熟于心才能随心所欲,内功到家,进阶武功才不会伤及自身。

Last updated