# 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)

## 总结

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

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryukiedev.gitbook.io/wiki/ios/di-ceng/19.methodswizzing-fang-fa-jiao-huan-de-keng.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
