> For the complete documentation index, see [llms.txt](https://ryukiedev.gitbook.io/wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ryukiedev.gitbook.io/wiki/ios/di-ceng/19.methodswizzing-fang-fa-jiao-huan-de-keng.md).

# 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` 替换父类中的方法 `functionA`， `functionA` 的实现变成了 `subFunctionA`。

### 2.2 调用原实现

一般我们交换方法后想要继续调用原本的实现一般会如[上文中functionB](https://ryukiedev.gitbook.io/wiki/ios/di-ceng/pages/-MgV8Oj6J_nDaxJH28U9#11-验证是否有效)那样调用一下自己。

那么我们在用子类中的方法 `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](/files/-MgV8Q3cR9Zsc967sUEF)

> 为什么呢？居然找不到了

#### 分析

我们在用子类中的方法 `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](/files/-MgV8Q3jZCf730Af2iQe)

* 父类实例&#x20;
  * `[ry functionA]`&#x20;
  * 调用没有受子类方法交换的影响
* 子类实例&#x20;
  * `[sub functionA]`
  * 没有出现父类调用找不到方法的情况

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

![死循环了](/files/-MgVNymejDraVtRMnP97)

### 4.1 分析

![04](/files/-MgVNymhfSt-xT071l8F)

由于并不存在 `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](/files/-MgVOupMH3snDGlpgYvn)

## 总结

黑魔法虽好，使用也得倍加小心！

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