从YYModel源码中可以学到什么:后篇

上一篇中《从YYModel源码中可以学到什么:后篇》中主要学习了YYModel的源码结构,只是分享了YYModel整体结构。
承接上篇,本文将解读YYModel如何进行JSON模型转换的,接下来一起揭开YYModel的神秘面纱吧!

目录

  • JSON -> Model
  • Model -> JSON

JSON转Model

JSON->Model

首先来看JSON是如何转为Model。查看YYModel的接口,提供了一个方法:

1
+ (instancetype)yy_modelWithJSON:(id)json;

注意jsonid类型,接收三种不同类型参数NSString,NSData,NSDictionay。下面是内部实现:

1
2
3
4
+ (instancetype)yy_modelWithJSON:(id)json {
NSDictionary *dic = [self _yy_dictionaryWithJSON:json];
return [self yy_modelWithDictionary:dic];
}

方法中调用了一个私有方法_yy_dictionaryWithJSON:,该方法将id类型的json(NSDictionary, NSString, NSData)转为字典。
JSON->Dictionay->Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (NSDictionary *)_yy_dictionaryWithJSON:(id)json {
// 验证json
if (!json || json == (id)kCFNull) return nil;
// 两个临时变量
NSDictionary *dic = nil;
NSData *jsonData = nil;
// 根据json类型,进行相应处理
if ([json isKindOfClass:[NSDictionary class]]) {
dic = json;
} else if ([json isKindOfClass:[NSString class]]) {
jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding];
} else if ([json isKindOfClass:[NSData class]]) {
jsonData = json;
}
// 使用NSJSONSerialization将Data转为字典
if (jsonData) {
dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
if (![dic isKindOfClass:[NSDictionary class]]) dic = nil;
}
return dic;
}

该方法很简单,就是判断id类型是指(NSDictionary,NSString,NSData)中的哪一个,分别处理。

json == (id)kCFNullkCFNull是什么意思?这条语句起到什么作用?

nil: 指向OC中对象的空指针

Nil: 指向OC中类的空指针

NULL:定义其他类型(基本类型、C类型)的空指针

NSNull:集合对象中,表示空值的对象。如给数组设置空值,使用NSNull,而不能使用nil

kCFNullNSNull的单例。

if (!json || json == (id)kCFNull) return nil;该判断的意思是,json对象不存在,或者为空是返回nil

JSON->Dictionay->Model

获取到字典后,调用字典转Model方法:

1
+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary

此方法也是在.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
/**
字典转model

@param dictionary 字典
@return 返回Model对象
*/
+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
// 1. 验证,空值,nil和是否是字典
if (!dictionary || dictionary == (id)kCFNull) return nil;
if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;

Class cls = [self class];
// 2. 创建一个YYModelMeta对象
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
// 判断是否需要自定义返回模型的类型,这是YYModel协议中的内容,算是附加功能暂时先忽略,后面在介绍。
if (modelMeta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
}
// 3. 创建model对象
NSObject *one = [cls new];

// 4. 关键方法
if ([one yy_modelSetWithDictionary:dictionary])
return one;

return nil;
}

这个方法主要的任务是调用了yy_modelSetWithDictionary:方法。这个方法也是在.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
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
// 1. 值和类型验证
if (!dic || dic == (id)kCFNull) return NO;
if (![dic isKindOfClass:[NSDictionary class]]) return NO;

// 2. 创建Modelmeta对象
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
// 属性个数,如为零返回。创建失败
if (modelMeta->_keyMappedCount == 0) return NO;
// YYModel协议,暂且忽略
if (modelMeta->_hasCustomWillTransformFromDictionary) {
dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
if (![dic isKindOfClass:[NSDictionary class]]) return NO;
}
// 3. 创建结构体
ModelSetContext context = {0};
context.modelMeta = (__bridge void *)(modelMeta);
context.model = (__bridge void *)(self);
context.dictionary = (__bridge void *)(dic);

// 模型元值数量和字典数量关系
// 1. 通常情况是两者相等,
// 2. 模型元键值少于字典个数
if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {

// 调用ModelSetWithDictionaryFunction方法,这是核心方法。
// 参数:
// 1. 要操作的字典
// 2. 为每个键值对调用一次的回调函数
// 3. 指针大小的程序定义的值,作为第三个参数传递给应用程序函数,但此函数未使用该值. 与参数2适应
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) {
// 每个keypath都执行ModelSetWithPropertyMetaArrayFunction
CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}

} else {
// 每个keypath都执行ModelSetWithPropertyMetaArrayFunction
CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
CFRangeMake(0, modelMeta->_keyMappedCount),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
// 忽略
if (modelMeta->_hasCustomTransformFromDictionary) {
return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
}
return YES;
}

上面注释该方法大致干了几件事。

  • 验证参数(为了程序健壮性一定要这么做)
  • 创建YYModelMeta对象
  • 创建ModelSetContext结构体
  • 为字典的每个键值对调用ModelSetWithDictionaryFunction方法
  • 验证结果

关于ModelSetContext是一个结构体。包含模型元,模型实例和待处理字典。

1
2
3
4
5
typedef struct {
void *modelMeta; ///< _YYModelMeta
void *model; ///< id (self)
void *dictionary; ///< NSDictionary (json)
} ModelSetContext;

通过上面的分析,线路越来越清晰,下面看一下核心方法ModelSetWithDictionaryFunction 将字典的键值对取出赋值给Model。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取到字典的键值对,和上下文信息。
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);
// 核心内容,遍历所有的属性元。知道_next = nil
while (propertyMeta) {
if (propertyMeta->_setter) {
// 最终转换(高潮)
ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
}
propertyMeta = propertyMeta->_next;
};
}

该方法主要任务是,遍历所有的属性元,调用模型属性赋值方法ModelSetValueForProperty

最终实现。也就是YYModel的最最最核心的部分。不过,这个方法长的离谱。由于涉及到较多的编码类型,需要对不同的类型区分处理,导致代码过长。

由于ModelSetValueForPorperty代码较长,这里不再复制代码。只梳理一下实现的逻辑,注释代码在Github自行查阅。

  • 元类型,isCNumber, nsType和其他类型来区分处理。
  • isCNumber此时调用方法ModelSetNumberToPorperty。该方法将NSNumber类型的值根据不同的编码赋值给属性。
  • nsTypeYYEncodingNSType枚举类型,枚举Foundation所有的类型,对不同的类型进行处理。
  • 最后是除上述的其他情况。YYEncodingType枚举定义的情况。

调用ModelSetNumberToPorperty方法,该方法作用是:识别nullbool123.23"123.45"等类型,转为NSNumber。

以上就是JSON转Model全部过程。

Flow

Model转JSON

Model->JSON

相对JSONModel来说,ModelJSON就简单多了。可以使用NSJSONSerialization类将Foundation对象转为JSON。但是转为JSON必须满足一下条件。

  • 顶层对象必须是NSArrayNSDictionary
  • 所有对象都是实例NSString,NSNumber,NSArray,NSDictionary或者NSNull
  • 所有字典键都是实例NSString
  • 数字不是NAN或无穷大

官方说明NSJSONSerialization文档

接下来看看是如何转换的,首先看到的方法是:

1
- (id)yy_modelToJSONObject;

该方法的时下很简单只有几行代码。

1
2
3
4
5
6
7
8
- (id)yy_modelToJSONObject {
// 关键方法
id jsonObject = ModelToJSONObjectRecursive(self);
if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject;
if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject;
return nil;
}

内部调用了方法ModelToJSONObjectRecursive方法。作者对该方法注释中说,返回一个有效的JSON对象(NSArray/NSDictionary/NSString/NSumber/NSNull)或者为空。查看源码,该方法做了几件事

  • 验证参数
  • 根据参数类型进行处理,NSString, NSNumber直接返回数据;字典,集合,数组类型,需要遍历所有元素递归处理;最后处理对象类型。
  • 返回不同类型的处理结果

总结

承接上文,本文对JSON转Model主线路进行详细的分析,关于一些辅助功能没有分析。还有,一些代码比较长,由于篇幅限制,所以注释代码放在《GitHub》

由于笔者能力有限,不可避免的出现错误,欢迎大家指正。