Runtime学习:实际应用

上一篇文章中介绍了 Runtime 的一些基本知识,以及方法传递的具体流程。这篇文章本想主要介绍 Runtime 的另一个核心概念——类的动态配置。但是,发现在写动态配置时,有许多实际应用的东西,索性直接写一篇实际应用吧。

本篇文章主要介绍几种 Runtime 的实际应用:

  • 关联对象(Associated Objects)
  • “黑魔法”(Method Swizzling)
    • 方法添加
    • 方法替换
  • 实现 NSCoding 的自动归/解档
  • 字典转模型

关联对象(Associated Objects)

一说到关联对象就联想到一个经典的面试题:“是否能通过 Category 给已有的类添加成员变量?”。

我们知道在分类中是不能够添加成员属性的,虽然我们用了 @property,但是仅仅会自动生成 get 和 set 方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过 Runtime 就可以做到给它方法的实现。

下面我们通过 Category 为 NSObject 添加一个 name 属性字符串。

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
---声明---
#import <Foundation/Foundation.h>

@interface NSObject (Name)

@property (nonatomic, strong) NSString *name;

@end

---实现---

#import "NSObject+ Name.h"
#import <objc/message.h>

@implementation NSObject (Name)

- (NSString *)name {
// 利用参数key 将对象object中存储的对应值取出来
return objc_getAssociatedObject(self, @"name");
}

- (void)setName:(NSString *)name {
// 将某个值跟某个对象关联起来,将某个值存储到某个对象中
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

---调用---
NSObject *objc = [[NSObject alloc]init];
objc.name = @"set name";

NSLog(@"runtime 动态添加属性:%@", objc.name);

---输出---
runtime 动态添加属性:set name

我们成功在分类上添加了一个属性,实现了它的 setter 和 getter 方法。 通过关联对象实现的属性的内存管理也是有 ARC 管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。

这里用到了两个方法:

1
2
3
id objc_getAssociatedObject(id object, const void *key);

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

object: 被关联的对象。

key: 关联的 key 值,要求唯一。

value: 关联的对象。

objc_AssociationPolicy: 内存管理策略。可以理解为 property 的修饰关键字。

“黑魔法”(Method Swizzling)

方法添加

动态添加方法的实现在上一篇讲述消息传递过程时,我们在动态解析阶段动态添加了方法,避免程序在未找到方法时的崩溃。

主要就是这个函数:

1
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

cls: 给哪个类添加方法。

name: 需要添加的方法名。 Objective-C 中可以直接使用 @selector(methodName) 得到方法名, Swift 中使用 #Selector(methodName)。

imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)。函数必须至少两个参数—— self 和 _cmd。

types: 参数以及返回值类型的字符串,需要用特定符号,参考官方文档Type encodings

方法替换

常见的方法替换的实际应用应该是无侵入埋点了——在保持原有方法功能的基础上,添加额外的功能。

下面通过一个例子来看看怎么玩这个大名鼎鼎的黑魔法。

实例: 在开发中,我们常用 [UIImage imageNamed:@”image”]; 方法来加载一张图片,但是我们不知道这个方法是否真的加载成功,在使用时需要进行一次判断。现在我们就给这个方法添加一些额外的功能(是否加载图片成功)。

代码实现:

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
38
39
40
41
42
43
44
45
46
#import "UIImage+Image.h"
#import <objc/message.h>

@implementation UIImage (Image)

/*
作用:把类加载进内存的时候调用,只会调用一次。
*/
+ (void)load {
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
// 1.获取 imageNamed方法地址
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
// 2.获取 ln_imageNamed方法地址
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(jt_imageNamed:));

// 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
});
}

/
下面的代码是不会有死循环的
调用 imageNamed 相当于调用 jt_imageNamed
调用 jt_imageNamed 相当于调用 imageNamed
*/
+ (UIImage *)jt_imageNamed:(NSString *)name {
UIImage *image = [UIImage jt_imageNamed: name];
if (image) {
NSLog(@"runtime交互方法 -> 图片加载成功");
} else {
NSLog(@"runtime交互方法 -> 图片加载失败");
}
return image;
}

@end

---调用---

UIImage *image = [UIImage imageNamed:@"Logo"];

---输出---

runtime交互方法 -> 图片加载成功

这里我们就替换了系统的实现,并且加入了我们自己的代码。这里我们可以在图片加载失败后,返回一个默认图片防止显示的空白了。

当我们为我们的程序进行埋点时,逻辑也是一样的,替换需要埋点的方法。这里举个简单的无侵入埋点的例子,代码如下:

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
38
39
40
41
42
// 这个 ViewController 作为程序内所有 ViewController 的基类
@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

NSLog(@"原有的");

UIImage *image = [UIImage imageNamed:@"LoginLogo"];

}

- (void)jt_viewDidLoad {

NSLog(@"埋点代码");

[self jt_viewDidLoad];
}

+ (void)load {
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
Class class = [self class];

SEL original = @selector(viewDidLoad);
SEL swizzled = @selector(jt_viewDidLoad);

Method originalMethod = class_getInstanceMethod(class, original);
Method swizzledMethod = class_getInstanceMethod(class, swizzled);

method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

@end

---输出---

埋点代码
原有的

实现 NSCoding 的自动归/解档

当我们需要存储我们自定义的一些类时,需要遵循 NSCoding 协议,并实现其归/解档的两个方法,但是当一个 model 的属性过多时,写代码就变成了重复的劳动,这里我们可以利用 Runtime 的一些方法来解决。

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
@implementation TestModel

- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[coder decodeObjectForKey:key] forKey:key];
}
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[coder encodeObject:[self valueForKey:key] forKey:key];
}
}

字典转模型

这个应该大部分人都已经在实际的项目中运用过了,我们使用的字典转模型的三方库——MJExtension,其实就是利用 Runtime 提供的函数遍历 Model 自身所有属性,如果属性在 json 中有对应的值,则将其赋值。

这个转化的过程,是利用了 Runtime 提供的函数以及一些 KVC 的知识点合作完成的。这里是我对KVC的原理的一些学习记录,字典转模型代码可以在我的github中查看哦。

小结

本来关于 Runtime 的知识点准备写三篇文章来记录的,但是关于类的动态配置的知识点写出来,感觉就像是 copy API 文档,所以就直接写了这篇实际应用。但是,自己立下的 flag,怎么也要完成吧。所以,计划下一篇文章写一写常见的 Runtime 的面试题吧。