揭秘 YYModel 的魔法 0x02

前言

在上文《揭秘 YYModel 的魔法(上)》 中主要剖析了 YYModel 的源码结构,并且分享了 YYClassInfo 与 NSObject+YYModel 内部有趣的实现细节。

紧接上篇,本文将解读 YYModel 关于 JSON 模型转换的源码,旨在揭秘 JSON 模型自动转换魔法。

索引

  • JSON 与 Model 相互转换
  • 总结

JSON 与 Model 相互转换

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,它易于人们阅读和编写,同时也易于机器解析和生成。它是基于 JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999 的一个子集。JSON 采用完全独立于语言的文本格式,但是也使用了类似于 C 语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使 JSON 成为理想的数据交换语言,点击 这里 了解更多关于 JSON 的信息。

Model 是 面向对象编程(Object Oriented Programming,简称 OOP)程序设计思想中的对象,OOP 把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。一般我们会根据业务需求来创建对象,在一些设计模式中(如 MVC 等)对象一般作为模型(Model),即对象建模。

JSON 与 Model 相互转换按转换方向分为两种:

  • JSON to Model
  • Model to JSON

JSON to Model

我们从 YYModel 的接口开始解读。

1
2
3
4
5
6
+ (instancetype)yy_modelWithJSON:(id)json {
// 将 json 转为字典 dic
NSDictionary *dic = [self _yy_dictionaryWithJSON:json];
// 再通过 dic 得到 model 并返回
return [self yy_modelWithDictionary:dic];
}

上面接口把 JSON 转 Model 很简单的分为了两个子任务:

  • JSON to NSDictionary
  • NSDictionary to Model

JSON to NSDictionary

我们先看一下 _yy_dictionaryWithJSON 是怎么将 json 转为 NSDictionary 的。

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
+ (NSDictionary *)_yy_dictionaryWithJSON:(id)json {
// 入参判空
if (!json || json == (id)kCFNull) return nil;

NSDictionary *dic = nil;
NSData *jsonData = nil;
// 根据 json 的类型对应操作
if ([json isKindOfClass:[NSDictionary class]]) {
// 如果是 NSDictionary 类则直接赋值
dic = json;
} else if ([json isKindOfClass:[NSString class]]) {
// 如果是 NSString 类则用 UTF-8 编码转 NSData
jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding];
} else if ([json isKindOfClass:[NSData class]]) {
// 如果是 NSData 则直接赋值给 jsonData
jsonData = json;
}

// jsonData 不为 nil,则表示上面的 2、3 情况中的一种
if (jsonData) {
// 利用 NSJSONSerialization 方法将 jsonData 转为 dic
dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
// 判断转换结果
if (![dic isKindOfClass:[NSDictionary class]]) dic = nil;
}

return dic;
}

这个函数主要是根据入参的类型判断如何将其转为 NSDictionary 类型并返回。

其中 kCFNull 是 CoreFoundation 中 CFNull 的单例对象。如同 Foundation 框架中的 NSNull 一样,CFNull 是用来表示集合对象中的空值(不允许为 NULL)。CFNull 对象既不允许被创建也不允许被销毁,而是通过定义一个 CFNull 常量,即 kCFNull,在需要空值时使用。

官方文档:
The CFNull opaque type defines a unique object used to represent null values in collection objects (which don’t allow NULL values). CFNull objects are neither created nor destroyed. Instead, a single CFNull constant object—kCFNull—is defined and is used wherever a null value is needed.

NSJSONSerialization 是用于将 JSON 和等效的 Foundation 对象之间相互转换的对象。它在 iOS 7 以及 macOS 10.9(包含 iOS 7 和 macOS 10.9)之后是线程安全的。

代码中将 NSString 转为 NSData 用到了 NSUTF8StringEncoding,其中编码类型必须属于 JSON 规范中列出的 5 种支持的编码类型:

  • UTF-8
  • UTF-16LE
  • UTF-16BE
  • UTF-32LE
  • UTF-32BE

而用于解析的最高效的编码是 UTF-8 编码,所以作者这里使用 NSUTF8StringEncoding。

官方注释:
The data must be in one of the 5 supported encodings listed in the JSON specification: UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE. The data may or may not have a BOM. The most efficient encoding to use for parsing is UTF-8, so if you have a choice in encoding the data passed to this method, use UTF-8.

NSDictionary to Model

现在我们要从 yy_modelWithJSON 接口中探究 yy_modelWithDictionary 是如何将 NSDictionary 转为 Model 的。

敲黑板!做好准备,这一小节介绍的代码是 YYModel 的精华哦~。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
// 入参校验
if (!dictionary || dictionary == (id)kCFNull) return nil;
if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;

// 使用当前类生成一个 _YYModelMeta 模型元类
Class cls = [self class];
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
// 这里 _hasCustomClassFromDictionary 用于标识是否需要自定义返回类
// 属于模型转换附加功能,可以不用投入太多关注
if (modelMeta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
}

// 调用 yy_modelSetWithDictionary 为新建的类实例 one 赋值,赋值成功则返回 one
NSObject *one = [cls new];
// 所以这个函数中我们应该把注意力集中在 yy_modelSetWithDictionary
if ([one yy_modelSetWithDictionary:dictionary]) return one;

return nil;
}

代码中根据 _hasCustomClassFromDictionary 标识判断是否需要自定义返回模型的类型。这段代码属于 YYModel 的附加功能,为了不使大家分心,这里仅做简单介绍。

如果我们要在 JSON 转 Model 的过程中根据情况创建不同类型的实例,则可以在 Model 中实现接口:

1
+ (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;

来满足需求。当模型元初始化时会检测当前模型类是否可以响应上面的接口,如果可以响应则会把 _hasCustomClassFromDictionary 标识为 YES,所以上面才会出现这些代码:

1
2
3
if (modelMeta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
}

嘛~ 我觉得这些附加的东西在阅读源码时很大程度上会分散我们的注意力,这次先详细的讲解一下,以后遇到类似的代码我们会略过,内部的实现大都与上述案例原理相同,感兴趣的同学可以自己研究哈。

我们应该把注意力集中在 yy_modelSetWithDictionary 上,这个函数(其实也是 NSObject+YYModel 暴露的接口)是根据字典初始化模型的实现方法。它的代码比较长,如果不想看可以跳过,在后面有解释。

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
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
// 入参校验
if (!dic || dic == (id)kCFNull) return NO;
if (![dic isKindOfClass:[NSDictionary class]]) return NO;

// 根据自身类生成 _YYModelMeta 模型元类
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
// 如果模型元类键值映射数量为 0 则 return NO,表示构建失败
if (modelMeta->_keyMappedCount == 0) return NO;

// 忽略,该标识对应 modelCustomWillTransformFromDictionary 接口
if (modelMeta->_hasCustomWillTransformFromDictionary) {
// 该接口类似 modelCustomTransformFromDictionary 接口,不过是在模型转换之前调用的
dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
if (![dic isKindOfClass:[NSDictionary class]]) return NO;
}

// 初始化模型设置上下文 ModelSetContext
ModelSetContext context = {0};
context.modelMeta = (__bridge void *)(modelMeta);
context.model = (__bridge void *)(self);
context.dictionary = (__bridge void *)(dic);

// 判断模型元键值映射数量与 JSON 所得字典的数量关系
if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
// 一般情况下他们的数量相等
// 特殊情况比如有的属性元会映射字典中的多个 key

// 为字典中的每个键值对调用 ModelSetWithDictionaryFunction
// 这句话是核心代码,一般情况下就是靠 ModelSetWithDictionaryFunction 通过字典设置模型
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
// 判断模型中是否存在映射 keyPath 的属性元
if (modelMeta->_keyPathPropertyMetas) {
// 为每个映射 keyPath 的属性元执行 ModelSetWithPropertyMetaArrayFunction
CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
// 判断模型中是否存在映射多个 key 的属性元
if (modelMeta->_multiKeysPropertyMetas) {
// 为每个映射多个 key 的属性元执行 ModelSetWithPropertyMetaArrayFunction
CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
} else { // 模型元键值映射数量少,则认为不存在映射多个 key 的属性元
// 直接为每个 modelMeta 属性元执行 ModelSetWithPropertyMetaArrayFunction
CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
CFRangeMake(0, modelMeta->_keyMappedCount),
ModelSetWithPropertyMetaArrayFunction,
&context);
}

// 忽略,该标识对应接口 modelCustomTransformFromDictionary
if (modelMeta->_hasCustomTransformFromDictionary) {
// 该接口用于当默认 JSON 转 Model 不适合模型对象时做额外的逻辑处理
// 我们也可以用这个接口来验证模型转换的结果
return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
}

return YES;
}

代码已经注明必要中文注释,关于两处自定义扩展接口我们不再多说,由于代码比较长我们先来梳理一下 yy_modelSetWithDictionary 主要做了哪些事?

  • 入参校验
  • 初始化模型元以及映射表校验
  • 初始化模型设置上下文 ModelSetContext
  • 为字典中的每个键值对调用 ModelSetWithDictionaryFunction
  • 检验转换结果

模型设置上下文 ModelSetContext 其实就是一个包含模型元,模型实例以及待转换字典的结构体。

1
2
3
4
5
typedef struct {
void *modelMeta; ///< 模型元
void *model; ///< 模型实例,指向输出的模型
void *dictionary; ///< 待转换字典
} ModelSetContext;

大家肯定都注意到了 ModelSetWithDictionaryFunction 函数,不论走哪条逻辑分支,最后都是调用这个函数把字典的 key(keypath)对应的 value 取出并赋值给 Model 的,那么我们就来看看这个函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 字典键值对建模
static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
// 拿到入参上下文
ModelSetContext *context = _context;
// 取出上下文中模型元
__unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
// 根据入参 _key 从模型元中取出映射表对应的属性元
__unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)];
// 拿到待赋值模型
__unsafe_unretained id model = (__bridge id)(context->model);
// 遍历 propertyMeta,直到 propertyMeta->_next == nil
while (propertyMeta) {
// 当前遍历的 propertyMeta 有 setter 方法,则调用 ModelSetValueForProperty 赋值
if (propertyMeta->_setter) {
// 核心方法,拎出来讲
ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
}
propertyMeta = propertyMeta->_next;
};
}

ModelSetWithDictionaryFunction 函数的实现逻辑就是先通过模型设置上下文拿到带赋值模型,之后遍历当前的属性元(直到 propertyMeta->_next == nil),找到 setter 不为空的属性元通过 ModelSetValueForProperty 方法赋值。

ModelSetValueForProperty 函数是为模型中的属性赋值的实现方法,也是整个 YYModel 的核心代码。别紧张,这个函数写得很友好的,也就 300 多行而已 😜(无关紧要的内容我会尽量忽略掉),不过忽略的太多会影响代码阅读的连续性,如果嫌长可以不看,文章后面会总结一下这个函数的实现逻辑。

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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
static void ModelSetValueForProperty(__unsafe_unretained id model,
__unsafe_unretained id value,
__unsafe_unretained _YYModelPropertyMeta *meta) {
// 如果属性是一个 CNumber,即输入 int、uint……
if (meta->_isCNumber) {
// 转为 NSNumber 之后赋值
NSNumber *num = YYNSNumberCreateFromID(value);
// 这里 ModelSetNumberToProperty 封装了给属性元赋值 NSNumber 的操作
ModelSetNumberToProperty(model, num, meta);
if (num) [num class]; // hold the number
} else if (meta->_nsType) {
// 如果属性属于 nsType,即 NSString、NSNumber……
if (value == (id)kCFNull) { // 为空,则赋值 nil(通过属性元 _setter 方法使用 objc_msgSend 将 nil 赋值)
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
} else { // 不为空
switch (meta->_nsType) {
// NSString 或 NSMutableString
case YYEncodingTypeNSString:
case YYEncodingTypeNSMutableString: {
// 处理可能的 value 类型:NSString,NSNumber,NSData,NSURL,NSAttributedString
// 对应的分支就是把 value 转为 NSString 或者 NSMutableString,之后调用 setter 赋值
...
} break;

// NSValue,NSNumber 或 NSDecimalNumber
case YYEncodingTypeNSValue:
case YYEncodingTypeNSNumber:
case YYEncodingTypeNSDecimalNumber: {
// 对属性元的类型分情况赋值(中间可能会涉及到类型之间的转换)
...
} break;

// NSData 或 NSMutableData
case YYEncodingTypeNSData:
case YYEncodingTypeNSMutableData: {
// 对属性元的类型分情况赋值(中间可能会涉及到类型之间的转换)
...
} break;

// NSDate
case YYEncodingTypeNSDate: {
// 考虑可能的 value 类型:NSDate 或 NSString
// 转换为 NSDate 之后赋值
...
} break;

// NSURL
case YYEncodingTypeNSURL: {
// 考虑可能的 value 类型:NSURL 或 NSString
// 转换为 NSDate 之后赋值(这里对 NSString 的长度判断是否赋值 nil)
...
} break;

// NSArray 或 NSMutableArray
case YYEncodingTypeNSArray:
case YYEncodingTypeNSMutableArray: {
// 对属性元的泛型判断
if (meta->_genericCls) { // 如果存在泛型
NSArray *valueArr = nil;
// value 所属 NSArray 则直接赋值,如果所属 NSSet 类则转为 NSArray
if ([value isKindOfClass:[NSArray class]]) valueArr = value;
else if ([value isKindOfClass:[NSSet class]]) valueArr = ((NSSet *)value).allObjects;

// 遍历刚才通过 value 转换来的 valueArr
if (valueArr) {
NSMutableArray *objectArr = [NSMutableArray new];
for (id one in valueArr) {
// 遇到 valueArr 中的元素属于泛型类,直接加入 objectArr
if ([one isKindOfClass:meta->_genericCls]) {
[objectArr addObject:one];
} else if ([one isKindOfClass:[NSDictionary class]]) {
// 遇到 valueArr 中的元素是字典类,
Class cls = meta->_genericCls;
// 忽略
if (meta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:one];
if (!cls) cls = meta->_genericCls; // for xcode code coverage
}
// 还记得我们直接的起点 yy_modelSetWithDictionary,将字典转模型
// 我觉得这应该算是一个间接递归调用
// 如果设计出的模型是无限递归(从前有座山,山上有座庙的故事),那么肯定会慢
NSObject *newOne = [cls new];
[newOne yy_modelSetWithDictionary:one];
// 转化成功机也加入 objectArr
if (newOne) [objectArr addObject:newOne];
}
}
// 最后将得到的 objectArr 赋值给属性
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, objectArr);
}
} else {
// 没有泛型,嘛~ 判断一下 value 的可能所属类型 NSArray 或 NSSet
// 转换赋值(涉及 mutable)
...
}
} break;

// NSDictionary 或 NSMutableDictionary
case YYEncodingTypeNSDictionary:
case YYEncodingTypeNSMutableDictionary: {
// 跟上面数组的处理超相似,泛型的间接递归以及无泛型的类型转换(mutable 的处理)
...
} break;

// NSSet 或 NSMutableSet
case YYEncodingTypeNSSet:
case YYEncodingTypeNSMutableSet: {
// 跟上面数组的处理超相似,泛型的间接递归以及无泛型的类型转换(mutable 的处理)
...
}

default: break;
}
}
} else { // 属性元不属于 CNumber 和 nsType
BOOL isNull = (value == (id)kCFNull);
switch (meta->_type & YYEncodingTypeMask) {
// id
case YYEncodingTypeObject: {
if (isNull) { // 空,赋值 nil
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
} else if ([value isKindOfClass:meta->_cls] || !meta->_cls) {
// 属性元与 value 从属于同一个类,则直接赋值
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)value);
} else if ([value isKindOfClass:[NSDictionary class]]) {
// 嘛~ value 从属于
NSObject *one = nil;
// 如果属性元有 getter 方法,则通过 getter 获取到实例
if (meta->_getter) {
one = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter);
}
if (one) {
// 用 yy_modelSetWithDictionary 输出化属性实例对象
[one yy_modelSetWithDictionary:value];
} else {
Class cls = meta->_cls;
// 略过
if (meta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:value];
if (!cls) cls = meta->_genericCls; // for xcode code coverage
}
// 用 yy_modelSetWithDictionary 输出化属性实例对象,赋值
one = [cls new];
[one yy_modelSetWithDictionary:value];
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)one);
}
}
} break;

// Class
case YYEncodingTypeClass: {
if (isNull) { // 空,赋值(Class)NULL,由于 Class 其实是 C 语言定义的结构体,所以使用 NULL
// 关于 nil,Nil,NULL,NSNull,kCFNull 的横向比较,我会单独拎出来在下面介绍
((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)NULL);
} else {
// 判断 value 可能的类型 NSString 或判断 class_isMetaClass(object_getClass(value))
// 如果满足条件则赋值
...
}
} break;

// SEL
case YYEncodingTypeSEL: {
// 判空,赋值(SEL)NULL
// 否则转换类型 SEL sel = NSSelectorFromString(value); 然后赋值
...
} break;

// block
case YYEncodingTypeBlock: {
// 判空,赋值(void (^)())NULL
// 否则判断类型 [value isKindOfClass:YYNSBlockClass()] 之后赋值
...
} break;

// struct、union、char[n],关于 union 共同体感兴趣的同学可以自己 google,这里简单介绍一下
// union 共同体,类似 struct 的存在,但是 union 每个成员会用同一个存储空间,只能存储最后一个成员的信息
case YYEncodingTypeStruct:
case YYEncodingTypeUnion:
case YYEncodingTypeCArray: {
if ([value isKindOfClass:[NSValue class]]) {
// 涉及 Type Encodings
const char *valueType = ((NSValue *)value).objCType;
const char *metaType = meta->_info.typeEncoding.UTF8String;
// 比较 valueType 与 metaType 是否相同,相同(strcmp(a, b) 返回 0)则赋值
if (valueType && metaType && strcmp(valueType, metaType) == 0) {
[model setValue:value forKey:meta->_name];
}
}
} break;

// void* 或 char*
case YYEncodingTypePointer:
case YYEncodingTypeCString: {
if (isNull) { // 判空,赋值(void *)NULL
((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, (void *)NULL);
} else if ([value isKindOfClass:[NSValue class]]) {
// 涉及 Type Encodings
NSValue *nsValue = value;
if (nsValue.objCType && strcmp(nsValue.objCType, "^v") == 0) {
((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, nsValue.pointerValue);
}
}
}

default: break;
}
}
}

额 😓 我是真的已经忽略掉很多代码了,没办法还是有点长。其实代码逻辑还是很简单的,只是模型赋值涉及的编码类型等琐碎逻辑比较多导致代码量比较大,我们一起来总结一下核心代码的实现逻辑。

  • 根据属性元类型划分代码逻辑
  • 如果属性元是 CNumber 类型,即 int、uint 之类,则使用 ModelSetNumberToProperty 赋值
  • 如果属性元属于 NSType 类型,即 NSString、NSNumber 之类,则根据类型转换中可能涉及到的对应类型做逻辑判断并赋值(可以去上面代码中查看具体实现逻辑)
  • 如果属性元不属于 CNumber 和 NSType,则猜测为 id,Class,SEL,Block,struct、union、char[n],void 或 char 类型并且做出相应的转换和赋值

嘛~ 其实上面的代码除了长以外逻辑还是很简单的,总结起来就是根据可能出现的类型去做出对应的逻辑操作,建议各位有时间还是去读下源码,尤其是自己项目中用到 YYModel 的同学。相信看完之后会对 YYModel 属性赋值一清二楚,这样在使用 YYModel 的日常中出现任何问题都可以心中有数,改起代码自然如有神助哈。

额…考虑到 NSDictionary to Model 的整个过程代码量不小,我花了一些时间将其逻辑总结归纳为一张图:

希望可以尽自己的努力让文章的表述变得更直白。

Model to JSON

相比于 JSON to Model 来说,Model to JSON 更简单一些。其中因为 NSJSONSerialization 在对 JSON 的转换时做了一些规定:

  • 顶级对象是 NSArray 或者 NSDictionary 类型
  • 所有的对象都是 NSString, NSNumber, NSArray, NSDictionary, 或 NSNull 的实例
  • 所有字典中的 key 都是一个 NSString 实例
  • Numbers 是除去无穷大和 NaN 的其他表示

Note: 上文出自 NSJSONSerialization 官方文档

知道了这一点后,我们就可以从 YYModel 的 Model to JSON 接口 yy_modelToJSONObject 处开始解读源码了。

1
2
3
4
5
6
7
8
- (id)yy_modelToJSONObject {
// 递归转换模型到 JSON
id jsonObject = ModelToJSONObjectRecursive(self);
if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject;
if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject;

return nil;
}

嘛~ 一共 4 行代码,只需要关注一下第一行代码中的 ModelToJSONObjectRecursive 方法,Objective-C 的语言特性决定了从函数名称即可无需注释看懂代码,这个方法从名字上就可以 get 到它是通过递归方法使 Model 转换为 JSON 的。

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
// 递归转换模型到 JSON,如果转换异常则返回 nil
static id ModelToJSONObjectRecursive(NSObject *model) {
// 判空或者可以直接返回的对象,则直接返回
if (!model || model == (id)kCFNull) return model;
if ([model isKindOfClass:[NSString class]]) return model;
if ([model isKindOfClass:[NSNumber class]]) return model;
// 如果 model 从属于 NSDictionary
if ([model isKindOfClass:[NSDictionary class]]) {
// 如果可以直接转换为 JSON 数据,则返回
if ([NSJSONSerialization isValidJSONObject:model]) return model;
NSMutableDictionary *newDic = [NSMutableDictionary new];
// 遍历 model 的 key 和 value
[((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description;
if (!stringKey) return;
// 递归解析 value
id jsonObj = ModelToJSONObjectRecursive(obj);
if (!jsonObj) jsonObj = (id)kCFNull;
newDic[stringKey] = jsonObj;
}];
return newDic;
}
// 如果 model 从属于 NSSet
if ([model isKindOfClass:[NSSet class]]) {
// 如果能够直接转换 JSON 对象,则直接返回
// 否则遍历,按需要递归解析
...
}
if ([model isKindOfClass:[NSArray class]]) {
// 如果能够直接转换 JSON 对象,则直接返回
// 否则遍历,按需要递归解析
...
}
// 对 NSURL, NSAttributedString, NSDate, NSData 做相应处理
if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString;
if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string;
if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model];
if ([model isKindOfClass:[NSData class]]) return nil;

// 用 [model class] 初始化一个模型元
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]];
// 如果映射表为空,则不做解析直接返回 nil
if (!modelMeta || modelMeta->_keyMappedCount == 0) return nil;
// 性能优化细节,使用 __unsafe_unretained 来避免在下面遍历 block 中直接使用 result 指针造成的不必要 retain 与 release 开销
NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64];
__unsafe_unretained NSMutableDictionary *dic = result;
// 遍历模型元属性映射字典
[modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
// 如果遍历当前属性元没有 getter 方法,跳过
if (!propertyMeta->_getter) return;

id value = nil;
// 如果属性元属于 CNumber,即其 type 是 int、float、double 之类的
if (propertyMeta->_isCNumber) {
// 从属性中利用 getter 方法得到对应的值
value = ModelCreateNumberFromProperty(model, propertyMeta);
} else if (propertyMeta->_nsType) { // 属性元属于 nsType,即 NSString 之类
// 利用 getter 方法拿到 value
id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
// 对拿到的 value 递归解析
value = ModelToJSONObjectRecursive(v);
} else {
// 根据属性元的 type 做相应处理
switch (propertyMeta->_type & YYEncodingTypeMask) {
// id,需要递归解析,如果解析失败则返回 nil
case YYEncodingTypeObject: {
id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = ModelToJSONObjectRecursive(v);
if (value == (id)kCFNull) value = nil;
} break;
// Class,转 NSString,返回 Class 名称
case YYEncodingTypeClass: {
Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = v ? NSStringFromClass(v) : nil;
} break;
// SEL,转 NSString,返回给定 SEL 的字符串表现形式
case YYEncodingTypeSEL: {
SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = v ? NSStringFromSelector(v) : nil;
} break;
default: break;
}
}
// 如果 value 还是没能解析,则跳过
if (!value) return;

// 当前属性元是 KeyPath 映射,即 a.b.c 之类
if (propertyMeta->_mappedToKeyPath) {
NSMutableDictionary *superDic = dic;
NSMutableDictionary *subDic = nil;
// _mappedToKeyPath 是 a.b.c 根据 '.' 拆分成的字符串数组,遍历 _mappedToKeyPath
for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) {
NSString *key = propertyMeta->_mappedToKeyPath[i];
// 遍历到结尾
if (i + 1 == max) {
// 如果结尾的 key 为 nil,则使用 value 赋值
if (!superDic[key]) superDic[key] = value;
break;
}

// 用 subDic 拿到当前 key 对应的值
subDic = superDic[key];
// 如果 subDic 存在
if (subDic) {
// 如果 subDic 从属于 NSDictionary
if ([subDic isKindOfClass:[NSDictionary class]]) {
// 将 subDic 的 mutable 版本赋值给 superDic[key]
subDic = subDic.mutableCopy;
superDic[key] = subDic;
} else {
break;
}
} else {
// 将 NSMutableDictionary 赋值给 superDic[key]
// 注意这里使用 subDic 间接赋值是有原因的,原因就在下面
subDic = [NSMutableDictionary new];
superDic[key] = subDic;
}
// superDic 指向 subDic,这样在遍历 _mappedToKeyPath 时即可逐层解析
// 这就是上面先把 subDic 转为 NSMutableDictionary 的原因
superDic = subDic;
subDic = nil;
}
} else {
// 如果不是 KeyPath 则检测 dic[propertyMeta->_mappedToKey],如果为 nil 则赋值 value
if (!dic[propertyMeta->_mappedToKey]) {
dic[propertyMeta->_mappedToKey] = value;
}
}
}];

// 忽略,对应 modelCustomTransformToDictionary 接口
if (modelMeta->_hasCustomTransformToDictionary) {
// 用于在默认的 Model 转 JSON 过程不适合当前 Model 类型时提供自定义额外过程
// 也可以用这个方法来验证转换结果
BOOL suc = [((id<YYModel>)model) modelCustomTransformToDictionary:dic];
if (!suc) return nil;
}

return result;
}

额…代码还是有些长,不过相比于之前 JSON to Model 方向上由 yy_modelSetWithDictionaryModelSetWithDictionaryFunctionModelSetValueForProperty 三个方法构成的间接递归来说算是非常简单了,那么总结一下上面的代码逻辑。

  • 判断入参,如果满足条件可以直接返回
  • 如果 Model 从属于 NSType,则根据不同的类型做逻辑处理
  • 如果上面条件不被满足,则用 Model 的 Class 初始化一个模型元 _YYModelMeta
  • 判断模型元的映射关系,遍历映射表拿到对应键值对并存入字典中并返回

Note: 这里有一个性能优化的细节,用 __unsafe_unretained 修饰的 dic 指向我们最后要 return 的 NSMutableDictionary *result,看作者的注释:// avoid retain and release in block 是为了避免直接使用 result 在后面遍历映射表的代码块中不必要的 retain 和 release 操作以节省开销。

总结

  • 文章紧接上文《揭秘 YYModel 的魔法(上)》中对 YYModel 代码结构的讲解后将重点放到了对 JSON 模型相互转换的实现逻辑上。
  • 从 JSON 模型的转换方向上划分,将 YYModel 的 JSON 模型转换过程正反方向剖析揭秘,希望可以解开大家对 JSON 模型自动转换的疑惑。

文章写得比较用心(是我个人的原创文章,转载请注明 https://lision.me/),如果发现错误会优先在我的 个人博客 中更新。如果有任何问题欢迎在我的微博 @Lision 联系我~

希望我的文章可以为你带来价值~

揭秘 YYModel 的魔法 0x01

前言

iOS 开发中少不了各种各样的模型,不论是采用 MVC、MVP 还是 MVVM 设计模式都逃不过 Model。

那么大家在使用 Model 的时候肯定遇到过一个问题,即接口传递过来的数据(一般是 JSON 格式)需要转换为 iOS 内我们能直接使用的模型(类)。iOS 开发早期第三方框架没有那么多,大家可能会手写相关代码,但是随着业务的扩展,模型的增多,这些没什么技术含量的代码只是在重复的浪费我们的劳动力而已。

这时候就需要一种工具来帮助我们把劳动力从这些无意义的繁琐代码中解放出来,于是 GitHub 上出现了很多解决此类问题的第三方库,诸如 Mantle、JSONModel、MJExtension 以及 YYModel 等等。

这些库的神奇之处在于它们提供了模型与 JSON 数据的自动转换功能,仿佛具有魔法一般!本文将通过剖析 YYModel 源码一步一步破解这“神奇”的魔法。

YYModel 是一个高性能 iOS/OSX 模型转换框架(该项目是 YYKit 组件之一)。YYKit 在我之前的文章【从 YYCache 源码 Get 到如何设计一个优秀的缓存】中已经很详细的介绍过了,感兴趣的同学可以点进去了解一下。

YYModel 是一个非常轻量级的 JSON 模型自动转换库,代码风格良好且思路清晰,可以从源码中看到作者对 Runtime 深厚的理解。难能可贵的是 YYModel 在其轻量级的代码下还保留着自动类型转换,类型安全,无侵入等特性,并且具有接近手写解析代码的超高性能。

处理 GithubUser 数据 10000 次耗时统计 (iPhone 6):

索引

  • YYModel 简介
  • YYClassInfo 剖析
  • NSObject+YYModel 探究
  • JSON 与 Model 相互转换
  • 总结

YYModel 简介

撸了一遍 YYModel 的源码,果然是非常轻量级的 JSON 模型自动转换库,加上 YYModel.h 一共也只有 5 个文件。

抛开 YYModel.h 来看,其实只有 YYClassInfo 和 NSObject+YYModel 两个模块。

  • YYClassInfo 主要将 Runtime 层级的一些结构体封装到 NSObject 层级以便调用。
  • NSObject+YYModel 负责提供方便调用的接口以及实现具体的模型转换逻辑(借助 YYClassInfo 中的封装)。

YYClassInfo 剖析

前面说到 YYClassInfo 主要将 Runtime 层级的一些结构体封装到 NSObject 层级以便调用,我觉得如果需要与 Runtime 层级的结构体做对比的话,没什么比表格来的更简单直观了:

YYClassInfo Runtime
YYClassIvarInfo objc_ivar
YYClassMethodInfo objc_method
YYClassPropertyInfo property_t
YYClassInfo objc_class

Note: 本次比较基于 Runtime 源码 723 版本。

安~ 既然是剖析肯定不会列个表格这样子哈。

YYClassIvarInfo && objc_ivar

我把 YYClassIvarInfo 看做是作者对 Runtime 层 objc_ivar 结构体的封装,objc_ivar 是 Runtime 中表示变量的结构体。

  • YYClassIvarInfo
1
2
3
4
5
6
7
8
9
@interface YYClassIvarInfo : NSObject
@property (nonatomic, assign, readonly) Ivar ivar; ///< 变量,对应 objc_ivar
@property (nonatomic, strong, readonly) NSString *name; ///< 变量名称,对应 ivar_name
@property (nonatomic, assign, readonly) ptrdiff_t offset; ///< 变量偏移量,对应 ivar_offset
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 变量类型编码,通过 ivar_getTypeEncoding 函数得到
@property (nonatomic, assign, readonly) YYEncodingType type; ///< 变量类型,通过 YYEncodingGetType 方法从类型编码中得到

- (instancetype)initWithIvar:(Ivar)ivar;
@end
  • objc_ivar
1
2
3
4
5
6
7
8
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE; // 变量名称
char * _Nullable ivar_type OBJC2_UNAVAILABLE; // 变量类型
int ivar_offset OBJC2_UNAVAILABLE; // 变量偏移量
#ifdef __LP64__ // 如果已定义 __LP64__ 则表示正在构建 64 位目标
int space OBJC2_UNAVAILABLE; // 变量空间
#endif
}

Note: 日常开发中 NSString 类型的属性我们都会用 copy 来修饰,而 YYClassIvarInfo 中的 nametypeEncoding 属性都用 strong 修饰。因为其内部是先通过 Runtime 方法拿到 const char * 之后通过 stringWithUTF8String 方法转为 NSString 的。所以即便是 NSString 这类属性在确定其不会在初始化之后被修改的情况下,使用 strong 做一次单纯的强引用在性能上讲比 copy 要高一些。

囧~ 不知道讲的这么细会不会反而引起反感,如果对文章有什么建议可以联系我 @薛定谔的猹

Note: 类型编码,关于 YYClassIvarInfo 中的 YYEncodingType 类型属性 type 的解析代码篇幅很长,而且没有搬出来的必要,可以参考官方文档 Type EncodingsDeclared Properties 阅读这部分源码。

YYClassMethodInfo && objc_method

相应的,YYClassMethodInfo 则是作者对 Runtime 中 objc_method 的封装,objc_method 在 Runtime 是用来定义方法的结构体。

  • YYClassMethodInfo
1
2
3
4
5
6
7
8
9
10
11
@interface YYClassMethodInfo : NSObject
@property (nonatomic, assign, readonly) Method method; ///< 方法
@property (nonatomic, strong, readonly) NSString *name; ///< 方法名称
@property (nonatomic, assign, readonly) SEL sel; ///< 方法选择器
@property (nonatomic, assign, readonly) IMP imp; ///< 方法实现,指向实现方法函数的函数指针
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 方法参数和返回类型编码
@property (nonatomic, strong, readonly) NSString *returnTypeEncoding; ///< 返回值类型编码
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *argumentTypeEncodings; ///< 参数类型编码数组

- (instancetype)initWithMethod:(Method)method;
@end
  • objc_method
1
2
3
4
5
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名称
char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法类型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法实现(函数指针)
}

可以看到基本也是一一对应的关系,除了类型编码的问题作者为了方便使用在封装时进行了扩展。

为了照顾对 Runtime 还没有一定了解的读者,我这里简单的解释一下 objc_method 结构体(都是我自己的认知,欢迎讨论):

  • SEL,selector 在 Runtime 中的表现形式,可以理解为方法选择器
1
typedef struct objc_selector *SEL;
  • IMP,函数指针,指向具体实现逻辑的函数
1
2
3
4
5
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif

关于更多 Runtime 相关的知识由于篇幅原因(真的写不完)就不在这篇文章介绍了,我推荐大家去鱼神的文章 Objective-C Runtime 学习(因为我最早接触 Runtime 就是通过这篇文章,笑~)。

有趣的是,鱼神的文章中对 SEL 的描述有一句“其实它就是个映射到方法的 C 字符串”,但是他在文章中没有介绍出处。本着对自己文章质量负责的原则,对于一切没有出处的表述都应该持有怀疑的态度,所以我下面讲一下自己的对于 SEL 的理解。

撸了几遍 Runtime 源码,发现不论是 objc-runtime-new 还是 objc-runtime-old 中都用 SEL 类型作为方法结构体的 name 属性类型,而且通过以下源码:

1
2
3
4
5
OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT const char * _Nonnull sel_getName(SEL _Nonnull sel)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

可以看到通过一个 const char * 类型的字符串即可在 Runtime 系统中注册并返回一个 SEL,方法的名称则会映射到这个 SEL。

官方注释:
Registers a method with the Objective-C runtime system, maps the method name to a selector, and returns the selector value.

所以我觉得 SEL 和 char * 的的确确是有某种一一对应的映射关系,不过 SEL 的本质是否是 char * 就要打一个问号了。因为我在调试 SEL 阶段发现 SEL 内还有一个当前 SEL 的指针,与 char * 不同的是当 char * 赋值之后当前 char * 变量指针指向字符串首字符,而 SEL 则是 ,即我们无法直接看到它。

所以我做了一个无聊的测试,用相同的字符串初始化一个 char * 实例与一个 SEL 实例,之后尝试打印它们,有趣的是不论我使用 %s 还是 %c 都可以从两个实例中得到相同的打印输出,不知道鱼神是否做过相同的测试(笑~)

嘛~ 经过验证我们可以肯定 SEL 和 char * 存在某种映射关系,可以相互转换。同时猜测 SEL 本质上就是 char *,如果有哪位知道 SEL 与 char * 确切关系的可以留言讨论哟。

YYClassPropertyInfo && property_t

YYClassPropertyInfo 是作者对 property_t 的封装,property_t 在 Runtime 中是用来表示属性的结构体。

  • YYClassPropertyInfo
1
2
3
4
5
6
7
8
9
10
11
12
13
@interface YYClassPropertyInfo : NSObject
@property (nonatomic, assign, readonly) objc_property_t property; ///< 属性
@property (nonatomic, strong, readonly) NSString *name; ///< 属性名称
@property (nonatomic, assign, readonly) YYEncodingType type; ///< 属性类型
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 属性类型编码
@property (nonatomic, strong, readonly) NSString *ivarName; ///< 变量名称
@property (nullable, nonatomic, assign, readonly) Class cls; ///< 类型
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *protocols; ///< 属性相关协议
@property (nonatomic, assign, readonly) SEL getter; ///< getter 方法选择器
@property (nonatomic, assign, readonly) SEL setter; ///< setter 方法选择器

- (instancetype)initWithProperty:(objc_property_t)property;
@end
  • property_t
1
2
3
4
struct property_t {
const char *name; // 名称
const char *attributes; // 修饰
};

为什么说 YYClassPropertyInfo 是作者对 property_t 的封装呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// runtime.h
typedef struct objc_property *objc_property_t;

// objc-private.h
#if __OBJC2__
typedef struct property_t *objc_property_t;
#else
typedef struct old_property *objc_property_t;
#endif

// objc-runtime-new.h
struct property_t {
const char *name;
const char *attributes;
};

这里唯一值得注意的就是 getter 与 setter 方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 先尝试获取属性的 getter 与 setter
case 'G': {
type |= YYEncodingTypePropertyCustomGetter;
if (attrs[i].value) {
_getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
}
} break;
case 'S': {
type |= YYEncodingTypePropertyCustomSetter;
if (attrs[i].value) {
_setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
}
} break;

// 如果没有则按照标准规则自己造
if (!_getter) {
_getter = NSSelectorFromString(_name);
}
if (!_setter) {
_setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]);
}

YYClassInfo && objc_class

最后作者用 YYClassInfo 封装了 objc_classobjc_class 在 Runtime 中表示一个 Objective-C 类。

  • YYClassInfo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface YYClassInfo : NSObject
@property (nonatomic, assign, readonly) Class cls; ///< 类
@property (nullable, nonatomic, assign, readonly) Class superCls; ///< 超类
@property (nullable, nonatomic, assign, readonly) Class metaCls; ///< 元类
@property (nonatomic, readonly) BOOL isMeta; ///< 元类标识,自身是否为元类
@property (nonatomic, strong, readonly) NSString *name; ///< 类名称
@property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< 父类(超类)信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; ///< 变量信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< 方法信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; ///< 属性信息

- (void)setNeedUpdate;
- (BOOL)needUpdate;

+ (nullable instancetype)classInfoWithClass:(Class)cls;
+ (nullable instancetype)classInfoWithClassName:(NSString *)className;

@end
  • objc_class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// objc.h
typedef struct objc_class *Class;

// runtime.h
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // isa 指针

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE; // 父类(超类)指针
const char * _Nonnull name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 版本
long info OBJC2_UNAVAILABLE; // 信息
long instance_size OBJC2_UNAVAILABLE; // 初始尺寸
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 变量列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法列表
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; // 缓存
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 协议列表
#endif

} OBJC2_UNAVAILABLE;

额… 看来想完全避开 Runtime 的知识来讲 YYModel 源码是不现实的。这里简单介绍一下 Runtime 中关于 Class 的知识以便阅读,已经熟悉这方面知识的同学就当温习一下好了。

  • isa 指针,用于找到所属类,类对象的 isa 一般指向对应元类。
  • 元类,由于 objc_class 继承于 objc_object,即类本身同时也是一个对象,所以 Runtime 库设计出元类用以表述类对象自身所具备的元数据。
  • cache,实际上当一个对象收到消息时并不会直接在 isa 指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了。为了优化方法调用的效率,加入了 cache,也就是说在收到消息时,会先去 cache 中查找,找不到才会去像上图所示遍历查找,相信苹果为了提升缓存命中率,应该也花了一些心思(笑~)。
  • version,我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

关于 Version 的官方描述:
Classes derived from the Foundation framework NSObject class can set the class-definition version number using the setVersion: class method, which is implemented using the class_setVersion function.

YYClassInfo 的初始化细节

关于 YYClassInfo 的初始化细节我觉得还是有必要分享出来的。

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
+ (instancetype)classInfoWithClass:(Class)cls {
// 判空入参
if (!cls) return nil;

// 单例缓存 classCache 与 metaCache,对应缓存类和元类
static CFMutableDictionaryRef classCache;
static CFMutableDictionaryRef metaCache;
static dispatch_once_t onceToken;
static dispatch_semaphore_t lock;
dispatch_once(&onceToken, ^{
classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
// 这里把 dispatch_semaphore 当做锁来使用(当信号量只有 1 时)
lock = dispatch_semaphore_create(1);
});

// 初始化之前,首先会根据当前 YYClassInfo 是否为元类去对应的单例缓存中查找
// 这里使用了上面的 dispatch_semaphore 加锁,保证单例缓存的线程安全
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls));
// 如果找到了,且找到的信息需要更新的话则执行更新操作
if (info && info->_needUpdate) {
[info _update];
}
dispatch_semaphore_signal(lock);

// 如果没找到,才会去老实初始化
if (!info) {
info = [[YYClassInfo alloc] initWithClass:cls];
if (info) { // 初始化成功
// 线程安全
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// 根据初始化信息选择向对应的类/元类缓存注入信息,key = cls,value = info
CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
dispatch_semaphore_signal(lock);
}
}

return info;
}

总结一下初始化的主要步骤:

  • 创建单例缓存,类缓存和元类缓存
  • 使用 dispatch_semaphore 作为锁保证缓存线程安全
  • 初始化前先去缓存中查找是否已经向缓存中注册过当前要初始化的 YYClassInfo
  • 如果查找到缓存对象,则判断缓存对象是否需要更新并执行相关操作
  • 如果缓存中未找到缓存对象则初始化
  • 初始化成功后向缓存中注册该 YYClassInfo 实例

其中,使用缓存可以有效减少我们在 JSON 模型转换时反复初始化 YYClassInfo 带来的开销,而 dispatch_semaphore 在信号量为 1 时是可以当做锁来使用的,虽然它在阻塞时效率超低,但是对于代码中的缓存阻塞这里属于低频事件,使用 dispatch_semaphore 在非阻塞状态下性能很高,这里锁的选择非常合适。

关于 YYClassInfo 的更新

首先 YYClassInfo 是作者对应 objc_class 封装出来的类,所以理应在其对应的 objc_class 实例发生变化时更新。那么 objc_class 什么时候会发生变化呢?

嘛~ 比如你使用了 class_addMethod 方法为你的模型类加入了一个方法等等。

YYClassInfo 有一个私有 BOOL 类型参数 _needUpdate 用以表示当前的 YYClassInfo 实例是否需要更新,并且提供了 - (void)setNeedUpdate; 接口方便我们在更改了自己的模型类时调用其将 _needUpdate 设置为 YES,当 _needUpdate 为 YES 时后面就不用我说了,相关的代码在上一节初始化中有哦。

1
2
3
if (info && info->_needUpdate) {
[info _update];
}

简单介绍一下 _update,它是 YYClassInfo 的私有方法,它的实现逻辑简单介绍就是清空当前 YYClassInfo 实例变量,方法以及属性,之后再重新初始化它们。由于 _update 实现源码并没有什么特别之处,我这里就不贴源码了。

嘛~ 对 YYClassInfo 的剖析到这里就差不多了。

NSObject+YYModel 探究

如果说 YYClassInfo 主要是作者对 Runtime 层在 JSON 模型转换中需要用到的结构体的封装,那么 NSObject+YYModel 在 YYModel 中担当的责任则是利用 YYClassInfo 层级封装好的类切实的执行 JSON 模型之间的转换逻辑,并且提供了无侵入性的接口。

第一次阅读 NSObject+YYModel.m 的源码可能会有些不适应,这很正常。因为其大量使用了 Runtime 函数与 CoreFoundation 库,加上各种类型编码和递归解析,代码量也有 1800 多行了。

我简单把 NSObject+YYModel.m 的源码做了一下划分,这样划分之后代码看起来一样很简单清晰:

  • 类型编码解析
  • 数据结构定义
  • 递归模型转换
  • 接口相关代码

类型编码解析

类型编码解析代码主要集中在 NSObject+YYModel.m 的上面部分,涉及到 YYEncodingNSType 枚举的定义,配套 YYClassGetNSType 函数将 NS 类型转为 YYEncodingNSType 还有 YYEncodingTypeIsCNumber 函数判断类型是否可以直接转为 C 语言数值类型的函数。

此外还有将 id 指针转为对应 NSNumber 的函数 YYNSNumberCreateFromID,将 NSString 转为 NSDate 的 YYNSDateFromString 函数,这类函数主要是方便在模型转换时使用。

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
static force_inline NSDate *YYNSDateFromString(__unsafe_unretained NSString *string) {
typedef NSDate* (^YYNSDateParseBlock)(NSString *string);
// YYNSDateFromString 支持解析的最长时间字符串
#define kParserNum 34
// 这里创建了一个单例时间解析代码块数组
// 为了避免重复创建这些 NSDateFormatter,它的初始化开销不小
static YYNSDateParseBlock blocks[kParserNum + 1] = {0};
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 这里拿 `yyyy-MM-dd` 举例分析
{
/*
2014-01-20 // Google
*/
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
formatter.dateFormat = @"yyyy-MM-dd";
// 这里使用 blocks[10] 是因为 `yyyy-MM-dd` 的长度就是 10
blocks[10] = ^(NSString *string) { return [formatter dateFromString:string]; };
}

// 其他的格式都是一样类型的代码,省略
...
});

if (!string) return nil;
if (string.length > kParserNum) return nil;
// 根据入参的长度去刚才存满各种格式时间解析代码块的单例数组取出对应的代码块执行
YYNSDateParseBlock parser = blocks[string.length];
if (!parser) return nil;
return parser(string);
#undef kParserNum
}

Note: 在 iOS 7 之前 NSDateFormatter 是非线程安全的。

除此之外还用 YYNSBlockClass 指向了 NSBlock 类,实现过程也比较巧妙。

1
2
3
4
5
6
7
8
9
10
11
12
13
static force_inline Class YYNSBlockClass() {
static Class cls;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
void (^block)(void) = ^{};
cls = ((NSObject *)block).class;
// 轮询父类直到父类指向 NSObject 停止
while (class_getSuperclass(cls) != [NSObject class]) {
cls = class_getSuperclass(cls);
}
});
return cls; // 拿到的就是 "NSBlock"
}

关于 force_inline 这种代码技巧,我说过我在写完 YYModel 或者攒到足够多的时候会主动拿出来与大家分享这些代码技巧,不过这里大家通过字面也不难理解,就是强制内联。

嘛~ 关于内联函数应该不需要我多说(笑)。

数据结构定义

NSObject+YYModel 中重新定义了两个类,通过它们来使用 YYClassInfo 中的封装。

NSObject+YYModel YYClassInfo
_YYModelPropertyMeta YYClassPropertyInfo
_YYModelMeta YYClassInfo

_YYModelPropertyMeta

_YYModelPropertyMeta 表示模型对象中的属性信息,它包含 YYClassPropertyInfo。

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
@interface _YYModelPropertyMeta : NSObject {
@package
NSString *_name; ///< 属性名称
YYEncodingType _type; ///< 属性类型
YYEncodingNSType _nsType; ///< 属性在 Foundation 框架中的类型
BOOL _isCNumber; ///< 是否为 CNumber
Class _cls; ///< 属性类
Class _genericCls; ///< 属性包含的泛型类型,没有则为 nil
SEL _getter; ///< getter
SEL _setter; ///< setter
BOOL _isKVCCompatible; ///< 如果可以使用 KVC 则返回 YES
BOOL _isStructAvailableForKeyedArchiver; ///< 如果可以使用 archiver/unarchiver 归/解档则返回 YES
BOOL _hasCustomClassFromDictionary; ///< 类/泛型自定义类型,例如需要在数组中实现不同类型的转换需要用到

/*
property->key: _mappedToKey:key _mappedToKeyPath:nil _mappedToKeyArray:nil
property->keyPath: _mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil
property->keys: _mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath _mappedToKeyArray:keys(array)
*/
NSString *_mappedToKey; ///< 映射 key
NSArray *_mappedToKeyPath; ///< 映射 keyPath,如果没有映射到 keyPath 则返回 nil
NSArray *_mappedToKeyArray; ///< key 或者 keyPath 的数组,如果没有映射多个键的话则返回 nil
YYClassPropertyInfo *_info; ///< 属性信息,详见上文 YYClassPropertyInfo && property_t 章节
_YYModelPropertyMeta *_next; ///< 如果有多个属性映射到同一个 key 则指向下一个模型属性元
}
@end

_YYModelMeta

_YYModelMeta 表示模型的类信息,它包含 YYClassInfo。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface _YYModelMeta : NSObject {
@package
YYClassInfo *_classInfo;
/// Key:被映射的 key 与 keyPath, Value:_YYModelPropertyMeta.
NSDictionary *_mapper;
/// Array<_YYModelPropertyMeta>, 当前模型的所有 _YYModelPropertyMeta 数组
NSArray *_allPropertyMetas;
/// Array<_YYModelPropertyMeta>, 被映射到 keyPath 的 _YYModelPropertyMeta 数组
NSArray *_keyPathPropertyMetas;
/// Array<_YYModelPropertyMeta>, 被映射到多个 key 的 _YYModelPropertyMeta 数组
NSArray *_multiKeysPropertyMetas;
/// 映射 key 与 keyPath 的数量,等同于 _mapper.count
NSUInteger _keyMappedCount;
/// 模型 class 类型
YYEncodingNSType _nsType;

// 忽略
...
}
@end

递归模型转换

NSObject+YYModel.m 内写了一些(间接)递归模型转换相关的函数,如 ModelToJSONObjectRecursive 之类的,由于涉及繁杂的模型编码解析以及代码量比较大等原因我不准备放在这里详细讲解。

我认为这种逻辑并不复杂但是牵扯较多的函数代码与结构/类型定义代码不同,后者更适合列出源码让读者对数据有全面清醒的认识,而前者结合功能实例讲更容易使读者对整条功能的流程有一个更透彻的理解。

所以我准备放到后面 JSON 与 Model 相互转换时一起讲。

接口相关代码

嘛~ 理由同上。

半章总结

  • 文章对 YYModel 源码进行了系统解读,有条理的介绍了 YYModel 的结构,相信会让各位对 YYModel 的代码结构有一个清晰的认识。
  • 深入剖析了 YYClassInfo 的 4 个类,并详细讲解了它们与 Runtime 层级结构体的对应。
  • 在剖析 YYClassInfo 章节中分享了一些我在阅读源码的过程中发现的并且觉得值得分享的处理细节,比如为什么作者选择用 strong 来修饰 NSString 等。顺便还对 SEL 与 char * 的关系做了实验得出了我的推论。
  • 把 YYClassInfo 的初始化以及更新细节单独拎出来做了分析。
  • 探究 NSObject+YYModel 源码(分享了一些实现细节)并对其实现代码做了划分,希望能够对读者阅读 YYModel 源码时提供一些小小的帮助。

嘛~ 上篇差不多就这样了。我写的上一篇 YYKit 源码系列文章【从 YYCache 源码 Get 到如何设计一个优秀的缓存】收到了不少的好评和支持(掘金里一位读者 @ios123456 的评论更是暖化了我),这些美好的东西让我更加坚定了继续用心创作文章的决心。

文章写得比较用心(是我个人的原创文章,转载请注明 https://lision.me/),如果发现错误会优先在我的 个人博客 中更新。如果有任何问题欢迎在我的微博 @Lision 联系我~

希望我的文章可以为你带来价值~