前言
我们都知道,开发中会有这么一个过程,就是将服务器返回的数据转换成我们自己定义的模型对象。当然服务器返回的数据结构有xml
类型的,也有json
类型的。本文只讨论json
格式的。
大家在项目中一般是怎么样将服务器返回的json
转化成自己定义的模型类呢?
我在项目中一般都是使用的MJExtension。
本文讲解的也基本就是解读MJExtension
中的部分源码。
好了,废话不多说,直接上代码,let’s go
简单字典转模型
首先,从最简单的字典开始,例如我们需要将如下的字典转化成自定义的模型。
1 | NSDictionary *dict = @{@"name":@"Scott", |
我们定义一个ScottUser
类,并且定义好属性名如下:
1 |
|
到此为止,我们下一步的目标就是拿到字典里面的值(value
)对ScottUser
模型属性进行赋值,模型的属性名对应着字典里面的key
。
最直接的方法就是:
1 | ScottUser *user = [[ScottUser alloc] init]; |
但是,对于每一次的数据转模型,你都要这样去写大量的重复代码,毫无意义。
当然我们利用setValuesForKeysWithDictionary:(NSDictionary *)dict
进行kvc
赋值。
KVC赋值
- 优点:不需要去手动一个一个属性赋值。
- 缺点:当自定义的属性和字典中的key不一样的时候,会报错。
- 解决办法:重写
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
方法。
我们可以通过写一个框架自动帮我们实现字典转模型,大致思路就是:
- 遍历模型中的
属性
,然后拿到属性名
作为键值
去字典中寻找值
; - 找到
值
后,根据模型的属性类型
将值
转化成正确的类型
; - 给
属性名
赋值。
遍历模型中的属性
,拿到属性名
作为键值
去字典中寻找值
方法伪代码:
[模型类 遍历属性的方法];
为了方便使用,创建一个叫NSObject+ScottProperty
的分类,写一个获取所有属性的方法。
1 |
|
假设我们在看不到一个类的.h
和.m
文件的前提下,有什么办法可以获取它所有的实例变量呢?
答案是通过Runtime
。
1 |
|
在外部调用+ (NSArray *)properties
方法能够打印出一个类的所有属性,如:
NSArray *arr = [ScottUser properties];
运行程序,能够看到控制台的输出:
从输出中可以看到通过property_getName()
获取每一个objc_property_t
的name
表示成员属性的名字,通过property_getAttributes()
获取每一个objc_property_t
的attributes
表示成员属性中的一些特性(如是什么类,原子性还是非原子性,是strong还是weak还是copy,生成的成员变量名等信息…)
从苹果的官方文档(Objective-C Runtime Programming Guide)可以得知,attributes
是一个类型编码字符串,这个字符串以T
作为开始,接上@encode
类型编码和一个逗号,以V
接上实例变量名作为结尾,在他们之间是一些其他信息,以逗号分隔,具体内容可以查看官方文档中详细的表格。
在实际赋值过程中,我们并不关心该属性的内存管理、生成的成员变量名、或者其他什么信息,在attributes
中,只需要知道它所属的类
或者知道什么基本数据类型
,即T
至第一个逗号之前
中间的内容,如果是类
的话还需要将@
和""
去掉。
实际上,Runtime
已经给我们提供获取属性名和属性特性的函数了,也就是通过property_getName()
和property_getAttributes()
。
这时候我们就可以获取到属性名和属性对应的属性特性了。
找到值
后,根据属性类型
将值
转化成正确的类型
现在已经完成了第一步,并且拿到了属性名
,但是数据类型还需要我们进一步截取,截取方法如下:
1 | for (int i=0; i<outCount; i++) { |
控制台结果显示,我们能够截取到其中的类型了:
回归到我们拿到这些属性类型
的初衷,是为了用字典中的值的类型
与模型中属性的类型
进行对比,想要对比,需要拿到属性的类型
,因此需要将这些编码转换成一个表示类的类,创建一个类用来包装类型。
1 |
|
OC对象可以通过Class
来表示类型,而基本数据类型只能用布尔来标识。
把这些名字和类型遍历出来,肯定是为了以后有用,所以需要把它们存起来,由于它们是一个”整体”,所以还是设计一个类将他们包装起来比较好,创建一个包装成员属性的类–ScottProperty
。
1 |
|
这时,代码就可以进行重构了,将属于不同类的功能封装到对应的类上,让ScottProperty
提供一个类方法用于返回一个将objc_property_t
进行包装的类。
1 | for (int i=0; i<outCount; i++) { |
propertyWithProperty:
方法实现如下:
1 | @implementation ScottProperty |
ScottPropertyType
也提供类方法用于包装类型:
1 |
|
重构完成之后,结构显得更加清晰,更加有利于接下来的工作,下面继续完成typeCode
的提取。
运行重构之后的代码,可以看到和重构之前是一样的:
上面提到的这些类型,是类型编码,在苹果文档中告诉我们编码对应的类型:
根据这个对应关系的图表,我们将常用的几个编码定义成常量字符串或者宏表示它所对应的类型,利于编码和阅读:
在ScottPropertyType
类定义以下属性类型:
1 | /** |
并写一个方法用于提取每个属性的类型:
1 | - (instancetype)initWithTypeString:(NSString *)typeString { |
到这里,我们一个ScottProperty
的骨架大致就搭好了。
在NSObject+ScottProperty
分类中遍历属性的时候,打印属性名和属性类型看看:
1 | for (int i=0; i<outCount; i++) { |
从图中可以看出,属于基本类型的属性打印出来的类型是null
,其他的都能正确打印出对应类型。
当我们想要使用字典转模型功能的时候,提供一个类方法方便转换,该方法放在NSObject+ScottKeyValue
分类中,该分类负责字典转模型的方法实现。
1 | + (instancetype)objectWithKeyValues:(id)keyValues { |
我们想要字典转模型的时候,直接如下使用:
1 | NSDictionary *dict = @{@"name":@"Scott", |
ok,运行程序,可以看到控制台输出ScottUser
类中各属性对应的类型:
我们进行下一步:用该属性名作为键去字典中寻找对应的值
伪代码:
[字典 valueForKey:属性名];
此处的属性名会有点问题,例如我们定义属性名的时候不能是关键字,而如果字典中的key
是涉及到关键字的,那么我们需要转换,但是也并非所有的都有这种情况,因此我们可以想到使用代理。我们在NSObject+ScottKeyValue
分类中写一个ScottKeyValue
协议,并且让它遵守该协议:
1 | @protocol ScottKeyValue <NSObject> |
然后我们提供一个类方法,用于处理将属性名与字典中的key
达到一致。
1 | + (NSString *)propertyKey:(NSString *)propertyName { |
调用:
1 | // 属性名作为键去寻找对应的值 |
运行,我们可以看到已经能够拿到值了:
接下来,我们拿到值后将值的类型转换为属性对应的数据类型。
首先需要处理数字类型,如果模型的属性是数字类型,即type.isNumberType == YES
,如果字典中的值是字符串类型,需要将其转成NSNumber类型,如果本来就是基本数据类型,则不用进行任何转换。
1 | if (type.isNumberType == YES) { |
其中有一种情况,是需要进行特殊处理的,当模型的属性是char
类型或者bool
类型时,获取到的编码都是c
,并且bool
还有可能是B
编码,它们都对应_boolType
,因为数字类型包含布尔类型,所以bool
类型要在数字类型的条件下进行额外判断。
1 | if (type.isNumberType == YES) { |
最后我们调用并打印
ScottUser *userModel = [ScottUser objectWithKeyValues:dict];
NSLog(@"name:%@,icon:%@,age:%d,height:%@,money:%@,sex:%ld,gay:%d",userModel.name,userModel.icon,userModel.age,userModel.height,userModel.money,(long)userModel.sex,userModel.gay);
到这里最简单的字典转模型大致完成了,当然还有很多的细节没有完善,后面再做处理。
JSON字符串转模型
定义一个json
字符串转成模型:
1 |
|
运行程序,这时程序会华丽丽的崩溃,因为程序原来只对字典类型作了处理:
1 | // 我们可以定位到程序崩溃在这里 |
所以在这之前需要将JSON
转成Foundation
框架中的对象,苹果提供了强大的NSJSONSerialization
,利用它,在刚开始传入字典/JSON
字符串的时候将其进行转换。
1 | - (instancetype)setKeyValues:(id)keyValues { |
该方法的实现如下,如果当前是字符串,则转换成NSData
再进行序列化。
1 | - (id)JSONObject { |
此时,运行程序,OK,能够看到控制台能正确输入结果:
复杂字典转模型
定义一个模型中包含模型的复杂字典:
1 | NSDictionary *dict = @{@"text":@"是啊,今天天气确实不错!", |
对待这种字典的思路,应该想到递归,当碰到模型中的属性类型是一个模型类时,将字典中的value
作为字典处理,然后再调用字典转模型的方法返回一个模型类,所以在包装类型时还要有个属性表示它是否是自定义的模型类,才能作为依据继续递归,判断的方法是看它是否来自于Foundation框架
的类。
在ScottPropertyType
中添加一个属性:
1 | /** 是否来源于Foundation框架,比如NSString,NSArray等 */ |
在- (void)getTypeCode:(NSString *)code
方法中添加这样一条:
1 | else if (code.length > 3 && [code hasPrefix:@"@\""]){ |
在NSObject+ScottProperty
分类中添加一个类方法:
1 | // 用于判断当前类是否来自于foundation框架 |
那么问题来了,如果判断是否来自于Foundation框架
呢? 下图展示了Foundation框架(NSObject部分)
下的类结构:
用一个NSSet
(比用NSArray
检索效率更高),返回一些常用基本的Foundation框架
下继承自NSObject
的类。
1 | static NSSet *foundationClasses_; |
所以判断是否是foundation框架
的类方法具体实现:
1 | + (BOOL)isClassFromFoundation:(Class)c { |
得到结果后,需要在NSObject+ScottKeyValue
分类中的setKeyValues:
方法中添加如下
1 | // 如果不是来自foundation框架的类并且不是基本数据类型 ,则递归,如果是基本数据类型,typeClass为nil |
到这里,复杂字典转模型就算是完成了,具体调用的过程看源码文章结尾会给地址。
字典数组转模型
稍微复杂的一种情况是一个字典里面带有数组:
1 | NSDictionary *dict = @{ |
上面定义了一个字典,创建一个ScottStatusResult
模型,里面有两个数组,另外还有其他3个键:
1 |
|
对于一个数组来说,你必须要告诉方法里面装的是什么模型,才能将字典中值为数组的成员转成模型。
在MJExtension
中,提供了两种方式进行处理。
方式一:调用
NSObject
分类中的类方法:1
2
3
4
5
6
7[ScottStatusResult setupObjectClassInArray:^NSDictionary *{
return @{ @"statuses" : @"ScottStatus",
// 或者 @"statuses" : [ScottStatus class],
@"ads" : @"ScottAd"
// 或者 @"ads" : [ScottAd class]
};
}];方式二:在模型的.m文件中实现方法供回调:
1
2
3
4
5
6
7
8
9+ (NSDictionary *)objectClassInArray
{
return @{
@"statuses" : @"ScottStatus",
// 或者 @"statuses" : [ScottStatus class],
@"ads" : @"ScottAd"
// 或者 @"ads" : [ScottAd class]
};
}
原理上都差不多,都是通过代码进行回调,这个主要实现方式二。
在NSObject+ScottKeyValue
分类中的ScottKeyValue
协议中添加一个方法
1 | + (NSDictionary *)objectClassInArray; |
在NSObject+ScottKeyValue
分类中的setKeyValues:
方法中添加一种类型判断
1 | // 如果不是来自foundation框架的类并且不是基本数据类型 ,则递归,如果是基本数据类型,typeClass为nil |
返回一个装了模型的数组方法实现:
1 | /** |
到这里,字典数组转模型就算是完成了,具体调用的过程看源码文章结尾会给地址。
key的替换
在实际开发中,服务器通常返回一个字段名id
,或者description
的JSON
数据,而这两个名字在OC
中有特殊含义,在定义属性的时候并不能使用这类名称.这时属性名与字典key
不再是直接对应的关系,需要加入一层转换。
这个在前面用该属性名作为键去字典中寻找对应的值讲到过,在次就不再重复讲解。
性能优化
将5个字典转模型的例子同时运行,在NSObject+ScottProperty
分类中的+ (NSArray *)properties
方法中添加一句打印NSLog(@"%@调用了properties方法",[self class]);
。另外,之前的例子都是有内存泄露的,这里添加了free(properties);
修复了这个问题。
1 | + (NSArray *)properties { |
运行程序,可以看到控制台输出:
可以看到,很多的类都不止一次调用了获取属性的方法,对于一个类来说,要获取它的全部属性,只要获取一次就够了.获取到后将结果缓存起来,下次就不必进行不必要的计算。
下面进行优化:
1 | // 设置一个全局字典用来将类的属性都缓存起来 |
将方法改写为:
1 | + (NSArray *)properties { |
此时,控制台输出:
可以看出每一个类只经过一次就可以获取所有属性。
除了缓存属性外,提取类型编码的过程也可以进一步缓存优化性能。
在下面的方法中加上一句打印:
1 | - (void)getTypeCode:(NSString *)code { |
可以看到控制台输出:
可以看到一些常用的类型例如NSString
多次调用了该方法。提取类型时,只要知道类名(在这里也就是typeCode
),一个ScottPropertyType
就已经可以确定了。
重写了- initWithTypeString:
方法:
1 | static NSMutableDictionary *cacheTypes_; |
输出结果:
结束语
OK,到这里,我们的解读也算是完成了,由于是下班之后写的,所以花费了4天的时间,终于把此篇文章写完了,欢迎大家点评并讨论。
最后代码地址:—>戳这里
参考资料
最后更新: 2023年12月03日 19:33:08
本文链接: http://example.com/post/f61e800.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!