OC底层原理21:Method-Swizzling方法交换

张建 lol

method-swizzling 是什么?

  • method-swizzling 的含义是 方法交换,其主要作用是 在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的 iOS黑魔法

  • 在OC中就是 利用method-swizzling实现AOP,其中 AOP(Aspect Oriented Programing,面向切面编程),区别于 OOP(面向对象编程)

    • OOPAOP 都是一种编程的思想
    • OOP 编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元
    • AOP 是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
  • 每个类 都维护者一个 方法列表,即 method_list,method_list 中有不同的 方法 Method,每个方法中包含了方法的 selIMP,方法交换就是 将sel和IMP原本的对应断开,并将sel和新的IMP生成对应关系

如下图所示,交换前后的 selIMP 的对应关系:

method-swizzling 涉及的相关API

  • 通过 sel 获取方法 Method

    • class_getInstanceMethod:获取实例方法
    • class_getClassMethod:获取类方法
  • method_getImplementation:获取一个方法的实现

  • method_setImplementation:设置一个方法的实现

  • method_getTypeEncoding:获取方法实现的编码类型

  • class_addMethod:添加方法实现

  • class_replaceMethod:用一个方法的实现,替换另一个方法的实现,即 aIMP 指向 bIMP,但是 bIMP 不一定指向 aIMP

  • method_exchangeImplementations:交换两个方法的实现,即 aIMP -> bIMPbIMP -> aIMP

坑点1:method-swizzling 使用过程中一次性的问题

所谓的一次性就是:method-swizzling 写在 load 方法中,而 load 方法会主动调用多次,这样会 导致方法的重复交换,使方法 sel 的指向又恢复成原来的 imp 的问题

【解决方案】

可以通过 单例设计 原则,使方法交换 只执行一次,在 OC 中可以通过 dispatch_once 实现单例

1
2
3
4
5
6
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[ZJRuntimeTool zj_methodSwizzlingWithClass:self oriSEL:@selector(helloworld) swizzledSEL:@selector(sayHello)];
});
}

坑点2:子类没有实现,父类实现了

在下面的这段代码中,ZJPerson 中实现了 personInstanceMethod,而 ZJStudent 继承自 ZJPerson,没有实现 personInstanceMethod,运行下面的这段代码会出现什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ***ZJPerson类****
@interface ZJPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation ZJPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
@end

// ******ZJStudent类******
@interface ZJStudent : ZJPerson
- (void)helloword;
+ (void)sayHello;
@end
@implementation ZJStudent
- (void)helloword{
NSLog(@"student对象方法:%s",__func__);
}
+ (void)sayHello{
NSLog(@"student类方法:%s",__func__);
}
@end

//*****VC****
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];

// 黑魔法坑点二:子类没有实现 - 父类实现
ZJStudent * student = [[ZJStudent alloc] init];
[student personInstanceMethod];

ZJPerson * person = [[ZJPerson alloc] init];
[person personInstanceMethod];
}
@end

其中,方法交换代码如下,是通过 ZJStudent 的分类 ZJ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ****ZJStudent+ZJ分类*****
@interface ZJStudent (ZJ)
@end
@implementation ZJStudent (ZJ)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[ZJRuntimeTool zj_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(zj_studentInstanceMethod)];
});
}

// personInstanceMethod 我需要父类的这个方法的一些东西
// 给你加一个personInstanceMethod 方法
// imp
- (void)zj_studentInstanceMethod{
// 是否会产生递归? 不会产生递归,原因是zj_studentInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去
[self zj_studentInstanceMethod];
NSLog(@"ZJStudent分类添加的zj对象方法:%s",__func__);
}
@end

下面是封装好的 method-swizzling 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface ZJRuntimeTool : NSObject
+ (void)zj_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;
@end
@implementation ZJRuntimeTool
+ (void)zj_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) {
NSLog(@"传入的交换类不能为空");
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
@end

通过实际代码的调试,发现会在p调用 personInstanceMethod 方法时崩溃,下面来进行详细说明

  • [student personInstanceMethod]; 中不报错是因为 student 中的 imp 交换成了 zj_studentInstanceMethod,而 ZJStudent 中有这个方法(在ZJ分类中),所以不会报错

  • 崩溃的点在于 [person personInstanceMethod];,其本质原因:ZJStudent分类ZJ 中进行了方法交换,将 person 中的 IMP 交换成了 ZJStudent 中的 zj_studentInstanceMethod,但是 ZJPerson 中没有 zj_studentInstanceMethod方法,即 相关的IMP找不到,所以就崩溃了

【优化:避免imp找不到】

通过 class_addMethod 尝试添加你要交换的方法

  • 如果 添加成功,即类中没有这个方法,则通过 class_replaceMethod 进行 替换,其内部会调用 class_addMethod 进行添加

  • 如果添加不成功,即类中有这个方法,则通过 method_exchangeImplementations 进行 交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (void)zj_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) {
NSLog(@"传入的交换类不能为空");
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

// 一般交换方法:交换自己有的方法
// 交换自己没有实现的方法:
// 首先:会先尝试给自己添加要交换的方法 - personInstanceMethod(SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(IMP) -> 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);
}
}

下面是 class_replaceMethod、class_addMethod、method_exchangeImplementations 的源码实现

其中 class_replaceMethodclass_addMethod 中都调用了 addMethod 方法,区别在于 bool 值的判断,下面是 addMethod 的源码实现。

坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?

在调用 personInstanceMethod 方法时,父类 ZJPerson 中只有声明,没有实现,子类 ZJStudent 中即没有声明,也没有实现

1
2
3
4
5
6
// 将父类 ZJPerson 实现注释
@implementation ZJPerson
//- (void)personInstanceMethod{
// NSLog(@"person对象方法:%s",__func__);
//}
@end

经过调试,发现运行代码会崩溃,报错结果如下所示:

原因是:栈溢出,递归死循环,那么为什么会发生递归呢?

主要是因为 personInstanceMethod 没有实现,然后在方法交换后,始终找不到 oriMethod,然后交换了个寂寞,即交换失败,当我们调用 personInstanceMethod(oriMethod)时,也就是 oriMethod 会进入 ZJzj_studentInstanceMethod 方法,然后这个方法中又调用了 zj_studentInstanceMethod,此时的 zj_studentInstanceMethod 并没有指向 oriMethod,然后导致了 自己调自己,即递归死循环

【优化:避免递归死循环】

  • 如果 oriMethod 为空,为了避免方法交换没有意义,而被废弃,需要做一些事情

    • 通过 class_addMethodoriSEL 添加 swiMethod 方法

    • 通过 method_setImplementationswiMethodIMP 指向 不做任何事的空实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+ (void)zj_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) {
NSLog(@"传入的交换类不能为空");
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

// 如果没有 oriMethod
if (!oriMethod) {
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self,SEL _cmd){}));
}

// 一般交换方法:交换自己有的方法
// 交换自己没有实现的方法:
// 首先:会先尝试给自己添加要交换的方法 - personInstanceMethod(SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(IMP) -> swizzledSEL
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

if (success) { // 自己没有 - 交换 - 没有父类进行处理(重写一个)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else { // 自己有
method_exchangeImplementations(oriMethod, swiMethod);
}
}

method-swizzling 类方法

类方法和实例方法的method-swizzling的原理是类似的,唯一的区别就是类方法存在 元类 中,所以可以做如下操作

  • ZJStudent 中只有类方法 sayHello 的声明,没有实现
1
2
3
4
5
@interface ZJStudent : ZJPerson
+ (void)sayHello;
@end
@implementation ZJStudent
@end
  • 在ZJStudent的分类的load方法中实现类方法的交换方法
1
2
3
4
5
6
7
8
9
10
11
//*****ZJStudent(ZJ)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[ZJRuntimeTool zj_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(sayHello) swizzledSEL:@selector(zj_studentClassMethod)];
});
}
+ (void)zj_studentClassMethod{
NSLog(@"ZJStudent分类添加的lg类方法:%s",__func__);
[[self class] zj_studentClassMethod];
}
  • 封装的 方法交换 如下:

    • 需要通过 class_getClassMethod 方法 获取类方法

    • 在调用 class_addMethodclass_replaceMethod 方法添加和替换时,需要传入的类是 元类,元类可以通过 object_getClass 方法获取类的元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ (void)zj_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");

Method oriMethod = class_getClassMethod([cls class], oriSEL);
Method swiMethod = class_getClassMethod([cls class], swizzledSEL);

if (!oriMethod) { // 避免动作没有意义
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
}

// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod

BOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

if (didAddMethod) {
class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
  • 调用如下
1
2
3
4
5
- (void)viewDidLoad {
[super viewDidLoad];

[ZJStudent sayHello];
}
  • 运行结果如下,由于符合 方法没有实现,所以或走到 空的imp
1
2
2022-03-14 15:48:59.225558+0800 Method-Swizzling[5512:3942308] ZJStudent分类添加的lg类方法:+[ZJStudent(ZJ) zj_studentClassMethod]
2022-03-14 15:48:59.225602+0800 Method-Swizzling[5512:3942308] 来了一个空的 imp

method-swizzling 应用

method-swizzling 最常用到的是 防止崩溃、字典、数组等越界

在iOS中,NSNumber、NSArray、NSDictionary 等这些都是类族,一个 NSArray 的实现可能由 多个类组成,所以如果想对 NSArray进行swizzling,必须获取到其“真身”进行swizzling,直接对NSArray进行操作是无效的

下面列举了 NSArray 和 NSDictionary 本类的类名,可以通过 Runtime 函数来取出本类

  • NSArray: __NSArrayI
  • NSMutableArray: __NSArrayM
  • NSDictionary: __NSDictionaryI
  • NSMutableDicionary: __NSDictionaryM

【以NSArray为例】

  • 创建一个NSArray分类 NSArray+ZJ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@implementation NSArray (ZJ)
// Swizzling核心代码
// 须要注意的是,好多同窗反馈下面代码不起做用,形成这个问题的缘由大多都是其调用了super load方法。在下面的load方法中,不该该调用父类的load方法。
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}

// 为了不和系统的方法冲突,我通常都会在swizzling方法前面加前缀
- (id)cm_objectAtIndex:(NSUInteger)index {
// 判断下标是否越界,若是越界就进入异常拦截
if (self.count-1 < index) {
@try {
return [self cm_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩溃后会打印崩溃信息。若是是线上,能够在这里将崩溃信息发送到服务器
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} // 若是没有问题,则正常进行方法调用
else {
return [self cm_objectAtIndex:index];
}
}
  • 打印结果如下,会输出崩溃日志,但是实际不会崩溃

  • Post title:OC底层原理21:Method-Swizzling方法交换
  • Post author:张建
  • Create time:2021-01-10 15:22:47
  • Post link:https://redefine.ohevan.com/2021/01/10/OC底层原理/OC底层原理21:Method-Swizzling方法交换/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.