Links

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];
输出:
01
为什么呢?居然找不到了

分析

我们在用子类中的方法 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)
02
  • 父类实例
    • [ry functionA]
    • 调用没有受子类方法交换的影响
  • 子类实例
    • [sub functionA]
    • 没有出现父类调用找不到方法的情况

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

死循环了

4.1 分析

04
由于并不存在 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);
}
}
05

总结

黑魔法虽好,使用也得倍加小心!
尤其要对方法的本质以及方法调用的流程烂熟于心才能随心所欲,内功到家,进阶武功才不会伤及自身。