iOS runtime详解

引导

runtime是运行时,对于从事iOS开发,想要深入学习OC的人,runtime是必须熟悉掌握的东西。

runtime的概念

Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了runtime 运行时来处理,可以说runtime是我们Objective-C幕后工作者。

  • runtime(简称运行时),是一套 纯C(C和汇编写的) 的API。而OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制
  • 对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。
  • OC的函数调用成为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
  • 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而C语言调用未实现的函数就会报错。

runtime的消息机制

  • 我们写的OC代码运行的时候也是转换成了runtime方式运行的。任何方法调用本质:就是发送一个消息(用runtime发送消息,OC底层实现通过runtime实现)。
  • 消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
  • 每一个OC的方法,底层必然有一个与之对应的runtime方法。

简单示例:
验证:方法调用,是否真的是转换为消息机制?
必须要导入头文件 #import<objc/message.h>
注解1:我们导入系统的头文件,一般用尖括号。
注解2:OC 解决消息机制方法提示步骤【查找build setting -> 搜索msg -> objc_msgSend(YES –> NO)】
注解3:最终生成消息机制,编译器做的事情,最终代码,需要把当前代码重新编译,用xcode编译器,【clang -rewrite-objc main.m 查看最终生成代码】,示例:cd main.m –> 输入前面指令,就会生成 .opp文件(C++代码)
注解4:这里一般不会直接导入
示例代码:OC 方法–>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
说明: eat(无参) 和 run(有参) 是 Person模型类中的私有方法「可以帮我调用私有方法」;
// Person *p = [Person alloc];
// 底层的实际写法
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
// p = [p init];
p = objc_msgSend(p, sel_registerName("init"));
// 调用对象方法(本质:让对象发送消息)
//[p eat];
// 本质:让类对象发送消息
objc_msgSend(p, @selector(eat));
objc_msgSend([Person class], @selector(run:),20);
//--------------------------- <#我是分割线#> ------------------------------//
// 也许下面这种好理解一点
// id objc = [NSObject alloc];
id objc = objc_msgSend([NSObject class], @selector(alloc));
// objc = [objc init];
objc = objc_msgSend(objc, @selector(init));

runtime 方法调用流程「消息机制」

消息机制方法调用流程

怎么去调用类方法和实例方法,实例方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
1.OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象对应的或其父类中查找方法。
2.注册方法编号(这里用方法编号的好处,可以快速查找)。
3.根据方法编号去查找对应方法。
4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

一个objc 对象的 isa 的指针指向什么?有什么作用?

每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

runtime 常见作用

  • 动态交换两个方法的实现
  • 动态添加属性
  • 实现字典转模型的自动转换
  • 发送消息
  • 动态添加方法
  • 拦截并替换方法
  • 实现 NSCoding 的自动归档和解档

runtime 常用开发应用场景「工作掌握」

runtime 交换方法

应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
需求:加载一张图片直接用[UIImage imageNamed:@”image”];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。
方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
方案二:使用 runtime,交换方法.
实现步骤:
1.给系统的方法添加分类
2.自己实现一个带有扩展功能的方法
3.交换方法,只需要交换一次

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
47
48
49
50
51
52
53
- (void)viewDidLoad {
[super viewDidLoad];
// 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
// 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
UIImage *image = [UIImage imageNamed:@"123"];
}
#import <objc/message.h>
@implementation UIImage (Image)
/**
load方法: 把类加载进内存的时候调用,只会调用一次
方法应先交换,再去调用
*/
+ (void)load {
// 1.获取 imageNamed方法地址
// class_getClassMethod(获取某个类的方法)
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
// 2.获取 ln_imageNamed方法地址
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
// 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}
/**
看清楚下面是不会有死循环的
调用 imageNamed => ln_imageNamed
调用 ln_imageNamed => imageNamed
*/
// 加载图片 且 带判断是否加载成功
+ (UIImage *)ln_imageNamed:(NSString *)name {
UIImage *image = [UIImage ln_imageNamed:name];
if (image) {
NSLog(@"runtime添加额外功能--加载成功");
} else {
NSLog(@"runtime添加额外功能--加载失败");
}
return image;
}
/**
不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
所以第二步,我们要 自己实现一个带有扩展功能的方法.
+ (UIImage *)imageNamed:(NSString *)name {
}
*/
@end
// 打印输出
2017-02-17 17:52:14.693 runtime[12761:543574] runtime添加额外功能--加载成功

总结:我们交换两个方法地址指向,必须在系统的imageNamed:方法调用前,所以讲代码卸载分类的load方法中,最后当运行的时候系统的方法就会去找我们的方法的实现。

runtime给分类动态添加属性

原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。
注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。

需求:给系统 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
@interface NSObject (Property)
// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
@property NSString *name;
@property NSString *height;
@end
@implementation NSObject (Property)
- (void)setName:(NSString *)name {
// objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
// object:给哪个对象添加属性
// key:属性名称
// value:属性值
// policy:保存策略
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @"name");
}
// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime动态添加属性name==%@",objc.name);
// 打印输出
2017-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123

总结:给属性赋值的本质其实就是让属性与一个对象产生关联,所以要个NSObject的分类的name属性赋值就是让name和NSObject产生关联,runtime可以做到这一点。

runtime字典转模型

字典转模型的方式:

  • 一个一个给模型属性赋值

  • 字典转模型KVC实现
    1、KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应
    2、如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]key找不到的错。
    3、分析:模型中的属性和字典中的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
    4、解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC字典转模型。

  • 字典转模型Runtime实现
    思路:利用运行时,遍历模型中的所有属性,根据模型中的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(注:字典中的取值,不一定会全部取出来)。

    考虑情况:
    1、当字典中的key 和模型的属性匹配不上。
    2、模型中嵌套模型(模型属性是另一个模型对象)。
    3、模型的属性是一个数组,数组中是一个个模型对象。

    注解:字典中的key和模型的属性不对应的情况有两种,一种是字典的键值对大于模型的属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应的值进行赋值,多余的键值对不需要去看;另外一种情况是模型属性数量大于字典中的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,只需加一个判断即可。

    实现步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。

    MJExtension字典转模型实现也是通过底层对runtime进行封装,才可以把模型中所有属性遍历出来。

字典转模型Runtime方式实现

1、runtime字典转为模型 – 字典中的key和模型的属性不匹配(模型属性数量大于字典键值对),代码如下:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
/**
class_copyIvarList: 获取类中的所有成员变量
Ivar:成员变量
第一个参数:表示获取哪个类中的成员变量
第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
count: 成员变量个数
*/
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
// 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】
// 而报错 (could not set nil as the value for the key age.)
if (value) {
// 给模型中属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}
```
**注**这里在获取模型类中的所有属性名,是采取 `class_copyIvarList` 先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(`去掉 _ ,从第一个角标开始截取`) 得到属性名。
原因:
`Ivar:成员变量,以下划线开头`,`Property 属性 `
获取类里面属性 `class_copyPropertyList`
获取类中的所有成员变量 `class_copyIvarList`
```objectivec
{
int _a; // 成员变量
}
@property (nonatomic, assign) NSInteger attitudes_count; // 属性
这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;

使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有settergetter方法的。

2、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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 替换: @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
// 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
//--------------------------- <#我是分割线#> ------------------------------//
//
// 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
// 判断下value是否是字典,并且是自定义对象才需要转换
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
// 字典转换成模型 userDict => User模型, 转换成哪个模型
// 根据字符串类名生成类对象
Class modelClass = NSClassFromString(ivarType);
if (modelClass) { // 有对应的模型才需要转
// 把字典转模型
value = [modelClass modelWithDict2:value];
}
}
// 给模型中属性赋值
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
```
3、runtime字典转模型--数组中装着模型(模型的属性是一个数组,数组中是字典模型对象),代码如下:
```objectivec
// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
//--------------------------- <#我是分割线#> ------------------------------//
//
// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
// arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 转换成id类型,就能调用任何对象的方法
id idSelf = self;
// 获取数组中字典对应的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel modelWithDict3:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错
if (value) {
// 给模型中属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}
```
**总结:**我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。
#### runtime添加方法
**应用场景:**如果一个类的方法非常多,加载类到内存的时候比较耗资源,需要给每个方法生成映射表,可以使用动态给某个类添加方法解决。
**注解:**OC中使用的懒加载,当用到的时候才去加载它,实际上只要一个类实现了某个方法,就会被加载到内存。当我们不想加载那么多方法的时候,就可以使用runtime动态的添加方法。
**需求:**runtime动态添加方法处理调用一个未实现的方法和去除报错。
**案例代码:**方法+调用+打印输出
```objectivec
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
// 默认person,没有实现run:方法,可以通过performSelector调用,但是会报错。
// 动态添加方法就不会报错
[p performSelector:@selector(run:) withObject:@10];
}
@implementation Person
// 没有返回值,1个参数
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
NSLog(@"跑了%@米", meter);
}
// 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
// 什么时候调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理
// 作用:动态添加方法,处理未实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// [NSStringFromSelector(sel) isEqualToString:@"run"];
if (sel == NSSelectorFromString(@"run:")) {
// 动态添加run方法
// class: 给哪个类添加方法
// SEL: 添加哪个方法,即添加方法的方法编号
// IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
// type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, sel, (IMP)aaa, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
// 打印输出
2017-02-17 19:05:03.917 runtime[12761:543574] runtime动态添加方法--跑了10

动态变量控制

现在有一个Person的类,创建xiaoming对象

  • 动态获取xiaoming类中的所有属性(包括私有的)
    Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);

  • 遍历属性找到对应的name字段
    const char *varName = ivar_getName(var);

  • 修改对应的字段值为20
    object_setIvar(self.xiaoMing, var, @"20");

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    -(void)answer{
    unsigned int count = 0;
    Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
    for (int i = 0; i<count; i++) {
    Ivar var = ivar[i];
    const char *varName = ivar_getName(var);
    NSString *name = [NSString stringWithUTF8String:varName];
    if ([name isEqualToString:@"_age"]) {
    object_setIvar(self.xiaoMing, var, @"20");
    break;
    }
    }
    NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
    }

####实现NSCoding的自动归档和解档
实现自定义的模型持久化的过程,如果一个模型有许多个属性,需要对每个属性都实现一遍encodeObjectdecodeObjectForKey方法,当遇到这样的模型有很多个,这是一件十分麻烦的事情,下面介绍简单的实现方法。

假设现在有一个Movie类,有3个属性。先看下 .h文件

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Movie.h文件
//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
@interface Movie : NSObject<NSCoding>
@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;
@end
```
如果是正常写法, .m 文件应该是这样的:
```objectivec
// Movie.m文件
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_movieId forKey:@"id"];
[aCoder encodeObject:_movieName forKey:@"name"];
[aCoder encodeObject:_pic_url forKey:@"url"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
self.movieId = [aDecoder decodeObjectForKey:@"id"];
self.movieName = [aDecoder decodeObjectForKey:@"name"];
self.pic_url = [aDecoder decodeObjectForKey:@"url"];
}
return self;
}
@end
```
如果这里有100个属性,难道我们也只能把100个属性都给写一遍吗。
使用runtime让我们有更简便的方法
```objectivec
#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Movie class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Movie class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
// 设置到成员变量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end
```
这样的方式实现,不管有多少个属性,写这几行代码就搞定了。如果嫌代码有点多,有更加简便的方法:两句代码搞定。
```objectivec
#import "Movie.h"
#import <objc/runtime.h>
#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
\
#define initCoderRuntime(A) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
\
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
encodeRuntime(Movie)
}
- (id)initWithCoder:(NSCoder *)decoder
{
initCoderRuntime(Movie)
}
@end

优化:上面是encodeWithCoder 和 initWithCoder这两个方法抽成宏。我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。

runtime下Class的各项操作

下面是 runtime 下Class的常见方法 及 带有使用示例代码。各项操作,【转载原著】http://www.jianshu.com/p/46dd81402f63

unsigned int count;

  • 获取属性列表
1
2
3
4
5
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
  • 获取方法列表
1
2
3
4
5
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
  • 获取成员变量列表
1
2
3
4
5
6
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
  • 获得协议列表
1
2
3
4
5
6
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}

现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法

  • 获得类方法
1
2
3
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
  • 获得实例方法
1
2
3
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 添加方法
1
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
  • 替换原方法实现
1
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  • 交换原方法实现
1
method_exchangeImplementations(oriMethod, cusMethod);

常用方法

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
// 得到类的所有方法
Method *allMethods = class_copyMethodList([Person class], &count);
// 得到所有成员变量
Ivar *allVariables = class_copyIvarList([Person class], &count);
// 得到所有属性
objc_property_t *properties = class_copyPropertyList([Person class], &count);
// 根据名字得到类变量的Ivar指针,但是这个在OC中好像毫无意义
Ivar oneCVIvar = class_getClassVariable([Person class], name);
// 根据名字得到实例变量的Ivar指针
Ivar oneIVIvar = class_getInstanceVariable([Person class], name);
// 找到后可以直接对私有变量赋值
object_setIvar(_per, oneIVIvar, @"Mike");//强制修改name属性
/* 动态添加方法:
第一个参数表示Class cls 类型;
第二个参数表示待调用的方法名称;
第三个参数(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction;
第四个参数表方法的参数,0代表没有参数;
*/
class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);
// 交换两个方法
method_exchangeImplementations(method1, method2);
// 关联两个对象
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/*
id object :表示关联者,是一个对象,变量名理所当然也是object
const void *key :获取被关联者的索引key
id value :被关联者,这里是一个block
objc_AssociationPolicy policy : 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
*/

runtime 几个参数概念

1、objc_msgSend
这是个最基本的用于发送消息的函数
其实编译器会根据情况在objc_msgSendobjc_msgSend_stret,objc_msgSendSuper, 或objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有Super的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。

2、SEL
objc_msgSend函数第二个参数类型为SEL,它是selectorObjc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器。

3、id
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

4、runtime.h中Class的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;//父类
const char *name OBJC2_UNAVAILABLE;//类名
long version OBJC2_UNAVAILABLE;//类版本
long info OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出)
long instance_size OBJC2_UNAVAILABLE;//实例大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;//存储每个实例变量的内存地址
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等
struct objc_cache *cache OBJC2_UNAVAILABLE;//缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;//协议
#endif
} OBJC2_UNAVAILABLE;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在objc_class结构体中:ivarsobjc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理。

面试题

1、什么是 method swizzling(俗称黑魔法)

  • 简单说就是进行方法交换

  • 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的

  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。

  • 交换方法的几种实现方式

    • 利用 method_exchangeImplementations 交换两个方法的实现
    • 利用 class_replaceMethod 替换方法的实现
    • 利用 method_setImplementation 来直接设置某个方法的IMP。

参考:
Runtime Method Swizzling开发实例汇总(持续更新中)
OC运行时黑魔法 Method Swizzling

2、下面的代码输出什么?

@implementation Son : NSObject
- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

答案:都输出 Son

  • class获取当前方法的调用者的类,superClass获取当前方法的调用者的父类,super仅仅是一个编译指示器,就是给编译器看的,不是一个指针。

  • 本质:只要编译器看到super这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用这个题目主要是考察关于objc中对selfsuper的理解:

  • self是类的隐藏参数,指向当前调用方法的这个类的实例。而super本质是一个编译器标示符,和self是指向的同一个消息接受者

  • 当使用self调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;

  • 而当使用super时,则从父类的方法列表中开始找。然后调用父类的这个方法

  • 调用[self class]时,会转化成objc_msgSend函数

id objc_msgSend(id self, SEL op, ...)
- 调用 `[super class]`时,会转化成 `objc_msgSendSuper` 函数.

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是 objc_super 这样一个结构体,其定义如下
 struct objc_super {
 __unsafe_unretained id receiver;
 __unsafe_unretained Class super_class;
 };

第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son

objc Runtime 开源代码对- (Class)class方法的实现
-(Class)class { return object_getClass(self); 
}

runtime模块推荐阅读文章

西木 http://www.jianshu.com/p/6b905584f536
天口三水羊 http://www.jianshu.com/p/9e1bc8d890f9
夜千寻墨 http://www.jianshu.com/p/46dd81402f63
袁峥Seemygo http://www.jianshu.com/p/e071206103a4
郑钦洪_ http://www.jianshu.com/p/bd24c3f3cd0a
HenryCheng http://www.jianshu.com/p/f6300eb3ec3d

程序员的最爱(不点进去你会后悔的)

【译文 & 源码】
【工具类】