这一系列的源码解析分享到现在已经是第五篇了。这五篇讲解的都是view层的一些流行的iOS开源框架。而从本篇开始开始,我打算要逐渐加深难度,讲解一些model层和网络层相关的开源框架。
想来想去,还是从JSONModel开始吧~
首先因为该框架还是比较流行的,在GitHub上也有将近6000颗星了,而且我自己对这个框架的使用也比较熟悉。还有一点是这个框架运用了运行时的相关知识,对想要了解运行时的童鞋还是很有帮助的。
该框架的核心代码并不是很多,主要还是错误类型判断和容错处理占了不少内容。读过一遍之后,感觉到作者思维的严谨性是非常值得我们学习的:作者专门建立了一个展示错误(NSError)的类,里面封装了很多错误类型,而且这个框架还允许用户根据自己的需求来自定义错误类型并阻止最终模型的生成,在后文会有详细讲解。
在讲解源码之前,有必要先给不会使用JSONModel的同学们通过实际的例子来介绍一下它的使用方法(而且后面的源码解析部分也是结合这些例子给出的,因为结合例子有助于加快理解):
使用方法
1. 最基本的使用
第一种就是单纯地传入一个字典,并转换成模型:
首先我们需要定义我们自己的模型类:1
2
3
4
5@interface Person : JSONModel
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) NSInteger age;
@end
然后再使用字典来转换为模型:1
2
3
4
5
6
7NSDictionary *dict = @{
@"name":@"Jack",
@"age":@23,
@"gender":@"male",
};
NSError *error;
Person *person = [[Person alloc] initWithDictionary:dict error:&error];
输出:1
2
3
4
5 <Person>
[name]: Jack
[sex]: male
[gender]: 23
</Person>
可以看出来,该框架的使用非常方便,一行代码就将模型转换好了。
但是该框架的功能远不止这些:
2. 转换属性名称
有的时候,传入的字典里的key发生了变化(比如接口重构之类的原因),但是我们前端这边已经写好的模型属性可能不容易被修改(因为业务逻辑很复杂什么的),所以这个时候,最好有一个转化的功能。
在这里举个例子:原来字典里的gender
这个key变成了sex
,这就需要我们定义一个转换的mapper(JSONKeyMapper
):1
2
3
4
5
6@implementation Person
+ (JSONKeyMapper *)keyMapper
{
return [[JSONKeyMapper alloc] initWithModelToJSONDictionary:@{
@"gender": @"sex", }];
}
这样一来,JSONKeyMapper
就会自动帮我们做转换。
为了验证效果,我们修改一下传入的字典里的gender
字段为sex
:1
2
3
4
5
6
7NSDictionary *dict = @{
@"name":@"Jack",
@"age":@23,
@"sex":@"male",
};
NSError *error;
Person *person = [[Person alloc] initWithDictionary:dict error:&error];
再看一下输出:
1 | <Person> |
没有受到传入字典里key值的变化的影响,是吧?
3. 自定义错误
除了一些框架里自己处理的错误(比如传入的对象不是字典等),框架的作者也允许我们自己定义属于我们自己的错误。
比如,当age
对应的数值小于25的时候,打印出Too young!
,并阻止模型的转换:
首先,我们在模型的实现文件里添加:
1 | - (BOOL)validate:(NSError **)error |
看一下结果:1
22017-02-20 16:14:54.217 jsonmodel_demo[32942:1967433] Too young!
2017-02-20 16:14:54.217 jsonmodel_demo[32942:1967433] (null)
打印了错误,而且模型也没有被转换。
4. 模型嵌套
有的时候,我们需要在模型里加一个数组,而这个数组里面的元素是另一个对象:这就涉及到了模型的嵌套。
举个例子,我们让上面的Person
对象含有一个数组Friends
,它里面的元素是对象Friend
,也就是好友信息。若要实现模型的嵌套,我们只需在原来的模型类里增加一个协议Friend
:
1 |
|
而且要在Person
的实现文件里加上这一段代码:1
2@implementation Friend
@end
注意!如果不添加,则会令程序崩溃。
最后,在使用的时候,我们只需将持有一个数组的字典里传入即可:
1 | NSArray *array = @[ |
输出结果:1
2
3
4
5
6
7
8<Person>
[age]: 23
[gender]: male
[friends]: (
"<Friend> \n [name]: Peter\n [age]: 35\n</Friend>"
)
[name]: Jack
</Person>
我们可以看到,person对象里含有一个数组,这个数组只有一个元素,对应着上面字典里的array里的信息。
OK,这样一来,大家已经可以掌握该框架的主要用法了,现在开始详细讲解代码:
源码解析
本篇源码解析主要围绕着initWithDictionary:error:
来展开,在这一个方法里作者做到了所有的容错和模型的转化。
按照老规矩,先上流程图:
该流程图对应的方法实现是:
1 | -(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err |
其中,
- 方法1-4:都是对错误的发现与处理。
- 方法5:是真正的mapping。
- 方法6:是作者给用户自己定义错误的方法,如果复合了用户自己定义的错误,那么即使mapping成功了,也要返回nil。
-方法7:成功返回模型对象。
在讲解代码之前,有必要先了解一下JSONModel所持有的一些数据:
关联对象kClassPropertiesKey:(用来保存所有属性信息的NSDictionary)
1
2
3
4
5{
age = "@property primitive age (Setters = [])";
name = "@property NSString* name (Standard JSON type, Setters = [])";
gender = "@property NSString* gender (Standard JSON type, Setters = [])";
}关联对象kClassRequiredPropertyNamesKey:(用来保存所有属性的名称NSSet)
1
2
3
4
5{(
name,
age,
gender
)}关联对象kMapperObjectKey:(用来保存JSONKeyMapper):自定义的mapper,具体的使用方法在上面的例子中可以看到。
- JSONModelClassProperty:封装的jsonmodel的一个属性,它包含了对应属性的名字(name:gender),类型(type:NSString),是否是JSONModel支持的类型(isStandardJSONType:YES/NO),是否是可变对象(isMutable:YES/NO)等属性。
再大致讲解一下整个的流程:
首先,在这个模型类的对象被初始化的时候,遍历自身到所有的父类(直到JSONModel为止),获取所有的属性,并将其保存在一个字典里。获取传入字典的所有key,将这些key与保存的所有属性进行匹配。如果匹配成功,则进行kvc赋值。
OK,现在从上到下逐步讲解上段代码:
首先,在load
方法里,定义了该框架支持的类型: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+(void)load
{
static dispatch_once_t once;
dispatch_once(&once, ^{
@autoreleasepool {
//兼容的对象属性
allowedJSONTypes = @[
[NSString class], [NSNumber class], [NSDecimalNumber class], [NSArray class], [NSDictionary class], [NSNull class], //immutable JSON classes
[NSMutableString class], [NSMutableArray class], [NSMutableDictionary class] //mutable JSON classes
];
//兼容的基本类型属性
allowedPrimitiveTypes = @[
@"BOOL", @"float", @"int", @"long", @"double", @"short",
//and some famous aliases
@"NSInteger", @"NSUInteger",
@"Block"
];
//转换器
valueTransformer = [[JSONValueTransformer alloc] init];
//自己的类型
JSONModelClass = NSClassFromString(NSStringFromClass(self));
}
});
}
然后我们看一下从方法3的init方法开始,作者都做了什么:
1 | -(id)init |
值得注意的是,这里的__inspectProperties:
方法是该框架的核心方法之一:它的任务是保存了所有需要赋值的属性。用作在将来与传进来字典进行映射:
1 | -(void)__inspectProperties |
需要注意几点:
- 作者利用一个
while
函数,获取当前类和当前类的除JSONModel的所有父类的属性保存在一个字典中。在将来用于和传入的字典进行映射。- 作者用
JSONModelClassProperty
类封装了JSONModel的每一个属性。这个类有两个重要的属性:一个是name
,它是属性的名称(例如gender)。另一个是type
,它是属性的类型(例如NSString)。- 作者将属性分为了如下几个类型:
- 对象(不含有协议)。
- 对象(含有协议,属于模型嵌套)。
- 基本数据类型。
- 结构体。
我们来看一下方法4的实现:
1 | -(BOOL)__doesDictionary:(NSDictionary*)dict matchModelWithKeyMapper:(JSONKeyMapper*)keyMapper error:(NSError**)err |
这里需要需要注意的:
- model类里面定义的属性集合是不能大于传入的字典里的key集合的。
- 如果存在了用户自定义的mapper,则需要按照用户的定义来进行转换。
(在这里是奖gender转换为了sex)。
最后来看一下本框架第二个核心代码(上面的方法5),也就是真正从字典里获取值并赋给当前模型对象的实现:
1 | -(BOOL)__importDictionary:(NSDictionary*)dict withKeyMapper:(JSONKeyMapper*)keyMapper validation:(BOOL)validation error:(NSError**)err |
值得注意的是:
- 作者在最后给属性赋值的时候使用的是kvc的
setValue:ForKey:
的方法。- 作者判断了模型里的属性的类型是否是JSONModel的子类,可见作者的考虑是非常周全的。
- 整个框架看下来,有很多的地方涉及到了错误判断,作者将将错误类型单独抽出一个类(
JSONModelError
),里面支持的错误类型很多,可以侧面反应作者思维之缜密。而且这个做法也可以在我们写自己的框架或者项目中使用。
错误判断的一个例子:1
2
3
4
5
6
7
8//JSONModelError.m
+(id)errorInvalidDataWithMessage:(NSString*)message
{
message = [NSString stringWithFormat:@"Invalid JSON data: %@", message];
return [JSONModelError errorWithDomain:JSONModelErrorDomain
code:kJSONModelErrorInvalidData
userInfo:@{NSLocalizedDescriptionKey:message}];
}
夸了作者这么多,唯一我个人不太喜欢的地方就是if语句下只有一行的时候,作者不喜欢加上大括号:1
2if (![jsonValue isEqual:[self valueForKey:property.name]])
[self setValue:jsonValue forKey:property.name];
但是我觉得应该加的:
1 | if (![jsonValue isEqual:[self valueForKey:property.name]]){ |
知识扩展
- 作者用NSScanner来扫描字符串,将从类结构体里拿过来的属性的描述字符串
T@\"NSString\",C,N,V_name
中扫描出了类型:NSString
。 - 作者两次用到了NSSet:当集合里的元素顺序不重要的时候,优先考虑用NSSet。
总的来说这个框架的难度还是不大的,但可能因为是第一次阅读不涉及UIVIiew的框架,感觉有些枯燥,不过慢慢习惯就好啦~
————————————————- 2018年7月17日更新 ————————————————-
注意注意!!!
笔者在近期开通了个人公众号,主要分享编程,读书笔记,思考类的文章。
- 编程类文章:包括笔者以前发布的精选技术文章,以及后续发布的技术文章(以原创为主),并且逐渐脱离 iOS 的内容,将侧重点会转移到提高编程能力的方向上。
- 读书笔记类文章:分享编程类,思考类,心理类,职场类书籍的读书笔记。
- 思考类文章:分享笔者平时在技术上,生活上的思考。
因为公众号每天发布的消息数有限制,所以到目前为止还没有将所有过去的精选文章都发布在公众号上,后续会逐步发布的。
而且因为各大博客平台的各种限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~
扫下方的公众号二维码并点击关注,期待与您的共同成长~