在开发中,对于处理网络请求中获取的数据(即把请求到的json或字典转换成方便使用的数据模型)是我们在开发中必不可少的操作。诸如强大的第三方MJExtension
、JSONModel
或者YYModel
是我们所熟知的框架,因为它们使用起来简单方便,简单到有时候一句代码就可以实现我们所需要的字典和模型之间的转换。但我们不能光会用,也要明白它们实现的原理。
最近学习了一下利用Runtime来实现iOS字典转换成模型的功能,参考了一些文章和资料,写了一个Demo地址,把学到的一些有关于Runtime字典转模型的知识分享一下。
基本原理:
基本原理就是利用Runtime可以获取模型中所有属性这一特性,来对要进行转换的字典进行遍历,利用KVC的- (nullable id)valueForKeyPath:(NSString *)keyPath;
和- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
方法去取出模型属性并作为字典中相对应的key,来取出其所对应的value,并把value赋值给模型属性。
实现过程:
这里所取出的value是要分三种情况的:
- value是正常的非集合(字典和数组)类型的参数,如:
@“Xiaoming”
、@18
。
@{
@"name" : @"Xiaoming",
@"age" : @18,
@"sex" : @"男"
}
对于这样的字典,我们可以直接用以下代码去实现:
#import "NSObject+Model.h"
#import
@implementation NSObject (Model)
+ (instancetype)ModelWithDict:(NSDictionary *)dict {
NSObject * obj = [[self alloc]init];
[obj transformDict:dict];
return obj;
}
- (void)transformDict:(NSDictionary *)dict {
Class cla = self.class;
// count:成员变量个数
unsigned int outCount = 0;
// 获取成员变量数组
Ivar *ivars = class_copyIvarList(cla, &outCount);
// 遍历所有成员变量
for (int i = 0; i < outCount; i++) {
// 获取成员变量
Ivar ivar = ivars[i];
// 获取成员变量名字
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil) continue;
// 利用KVC将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
//需要释放指针,因为ARC不适用C函数
free(ivars);
}
以上代码每句都有注释,就不详细介绍了。需要注意的是:以上代码是写在#import "NSObject+Model.h”
分类中的,方便其他类调用,而且要引入头文件#import
。其中的if (value == nil) continue;
是为了防止如果模型属性数量大于字典键值对数量,模型属性会被赋值为nil而报错。
- value是字典,意思就是字典中包含字典,如:
@{
@"name" : @"Xiaoming",
@"age" : @18,
@"sex" : @"男",
@"city" : @"北京市",
@"school" : @{
@"name" : @"海淀一中",
@"address" : @"海淀区",
@"grade" : @{
@"name" : @"九年级",
@"teacher" : @"Mr Li"
}
}
}
对于这样的字典,我们要在字典中的值赋到模型上之前,利用Runtime的ivar_getTypeEncoding
方法来获取模型对象类型,并利用递归(如果不知道递归是什么,可以看我的另一篇文章递归算法或自行百度)的方式对该类型再进行字典转模型。代码如下:
#import "NSObject+Model.h"
#import
@implementation NSObject (Model)
+ (instancetype)ModelWithDict:(NSDictionary *)dict {
NSObject * obj = [[self alloc]init];
[obj transformDict:dict];
return obj;
}
- (void)transformDict:(NSDictionary *)dict {
Class cla = self.class;
// count:成员变量个数
unsigned int outCount = 0;
// 获取成员变量数组
Ivar *ivars = class_copyIvarList(cla, &outCount);
// 遍历所有成员变量
for (int i = 0; i < outCount; i++) {
// 获取成员变量
Ivar ivar = ivars[i];
// 获取成员变量名字
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil) continue;
// 获得成员变量的类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 如果属性是对象类型(字典)
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
// 那么截取对象的名字(比如@"School",截取为School)
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
// 排除系统的对象类型(如果人为的设置自定义的类带@”NS“如:NSSchool,则会出现错误)
if (![type hasPrefix:@"NS"]) {//字典
// 将对象名转换为对象的类型,将新的对象字典转模型(递归),如Grade,并将其对象grade对应的字典转换成模型
Class class = NSClassFromString(type);
value = [class ModelWithDict:value];
}
}
// 利用KVC将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
//需要释放指针,因为ARC不适用C函数
free(ivars);
}
- value是数组,意思就是字典中包含数组,数组中又包含字典,如下:
@{
@"name" : @"Xiaoming",
@"age" : @18,
@"sex" : @"男",
@"city" : @"北京市",
@"lessons" : @[@{
@"name" : @"语文",
@"score" : @125
},
@{
@"name" : @"数学",
@"score" : @146
},
@{
@"name" : @"英语",
@"score" : @112
}]
};
对于这样的字典,利用和上面一样的方法,即利用Runtime的ivar_getTypeEncoding
方法来获取模型对象类型,该对象模型就是我们在其上层模型类中所设置的接收字典中数组的对象类型,即@property(nonatomic,strong)NSArray * lessons;
中的NSArray类型。对数组中每个模型遍历并字典转模型,但我们并不知道每个模型的数据类型,这就需要在分类中声明一个返回该模型类型的方法- (NSString *)gainClassType;
,并在接收数组中的字典转换成的模型类中重写该方法,并返回模型数据类型,如下:
//重写gainClassType方法返回的数组中字典模型对应的类型:Lesson
- (NSString *)gainClassType {
return @"Lesson";
}
在第二种情况的基础上添加一层value是数组的判断,代码如下:
#import "NSObject+Model.h"
#import
@implementation NSObject (Model)
+ (instancetype)ModelWithDict:(NSDictionary *)dict {
NSObject * obj = [[self alloc]init];
[obj transformDict:dict];
return obj;
}
- (void)transformDict:(NSDictionary *)dict {
Class cla = self.class;
// count:成员变量个数
unsigned int outCount = 0;
// 获取成员变量数组
Ivar *ivars = class_copyIvarList(cla, &outCount);
// 遍历所有成员变量
for (int i = 0; i < outCount; i++) {
// 获取成员变量
Ivar ivar = ivars[i];
// 获取成员变量名字
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil) continue;
// 获得成员变量的类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 如果属性是对象类型(字典或者数组中包含字典)
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
// 那么截取对象的名字(比如@"School",截取为School)
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
// 排除系统的对象类型(如果人为的设置自定义的类带@”NS“如:NSSchool,则会出现错误)
if (![type hasPrefix:@"NS"]) {//字典
// 将对象名转换为对象的类型,将新的对象字典转模型(递归),如Grade,并将其对象grade对应的字典转换成模型
Class class = NSClassFromString(type);
value = [class ModelWithDict:value];
}else if ([type isEqualToString:@"NSArray"]) {//数组中包含字典
// 如果是数组类型,将数组中的每个模型进行字典转模型,先创建一个临时数组存放模型
NSArray *array = (NSArray *)value;
NSMutableArray *mArray = [NSMutableArray array];
// 获取到每个模型的类型
id class ;
if ([self respondsToSelector:@selector(gainClassType)]) {
//获取数组中每个字典对应转换的类型,即重写gainClassType方法返回的类型:Lesson
NSString *classStr = [self gainClassType];
class = NSClassFromString(classStr);
}
// 将数组中的所有模型进行字典转模型
for (int i = 0; i < array.count; i++) {
[mArray addObject:[class ModelWithDict:value[i]]];
}
value = mArray;
}
}
// 利用KVC将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
//需要释放指针,因为ARC不适用C函数
free(ivars);
}
上述代码便实现了三种情况的组合字典转模型的方法,然后在ViewController
中调用字典转模型的方法,如下:
People * p = [People ModelWithDict:self.dict];
Lesson * l = [p.lessons lastObject];
School * s = p.school;
Grade * g = s.grade;
NSLog(@"People:%@\n",p);
NSLog(@"Lesson:%@\n",l);
NSLog(@"School:%@\n",s);
NSLog(@"Grade:%@\n",g);
NSLog(@"teacher:%@",p.school.grade.teacher);
打印结果为:
2018-08-08 11:21:36.198147+0800 WXQModel_Runtime[2488:99552] People:
2018-08-08 11:21:36.198389+0800 WXQModel_Runtime[2488:99552] Lesson:
2018-08-08 11:21:36.198533+0800 WXQModel_Runtime[2488:99552] School:
2018-08-08 11:21:36.198733+0800 WXQModel_Runtime[2488:99552] Grade:
2018-08-08 11:21:36.199065+0800 WXQModel_Runtime[2488:99552] teacher:Mr Li
从结果中发现我们并不能直接打印出模型类中的所有属性值,这就需要我们去重写所有模型类的- (NSString *)description;
方法。创建一个父类@interface BaseModel : NSObject
,导入头文件#import
,重写description方法,如下:
#import "BaseModel.h"
#import
@implementation BaseModel
- (NSString *)description {
unsigned int count;
const char *clasName = object_getClassName(self);
NSMutableString *string = [NSMutableString stringWithFormat:@"<%s: %p>:[ \n",clasName, self];
Class clas = NSClassFromString([NSString stringWithCString:clasName encoding:NSUTF8StringEncoding]);
Ivar *ivars = class_copyIvarList(clas, &count);
for (int i = 0; i < count; i++) {
@autoreleasepool {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
//得到类型
NSString *type = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
NSString *key = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
id value = [self valueForKey:key];
//确保BOOL 值输出的是YES 或 NO,这里的B是我打印属性类型得到的……
if ([type isEqualToString:@"B"]) {
value = (value == 0 ? @"NO" : @"YES");
}
[string appendFormat:@"\t%@ = %@\n",[self delLine:key], value];
}
}
[string appendFormat:@"]"];
return string;
}
//去掉下划线
- (NSString *)delLine:(NSString *)string {
if ([string hasPrefix:@"_"]) {
return [string substringFromIndex:1];
}
return string;
}
@end
让所有的数据模型继承BaseModel
类,如:@interface People : BaseModel
,然后在重新打印结果:
2018-08-08 11:27:55.873652+0800 WXQModel_Runtime[2610:105369] People::[
age = 18
name = Xiaoming
sex = 男
school = :[
name = 海淀一中
address = 海淀区
grade = :[
name = 九年级
teacher = Mr Li
]
]
lessons = (
":[ \n\tname = \U8bed\U6587\n\tscore = 125\n]",
":[ \n\tname = \U6570\U5b66\n\tscore = 146\n]",
":[ \n\tname = \U82f1\U8bed\n\tscore = 112\n]"
)
]
2018-08-08 11:27:55.873911+0800 WXQModel_Runtime[2610:105369] Lesson::[
name = 英语
score = 112
]
2018-08-08 11:27:55.874120+0800 WXQModel_Runtime[2610:105369] School::[
name = 海淀一中
address = 海淀区
grade = :[
name = 九年级
teacher = Mr Li
]
]
2018-08-08 11:27:55.874280+0800 WXQModel_Runtime[2610:105369] Grade::[
name = 九年级
teacher = Mr Li
]
2018-08-08 11:27:55.874462+0800 WXQModel_Runtime[2610:105369] teacher:Mr Li
欢迎指正交流0_0~