前言
我们在iOS开发中,一般会使用MVC或者MVVM等模式。当我们从接口中拿到数据时,我们需要把数据转成模型使用。下面我就带大家一起用runtime一步一步的来完成这个转换框架.
1、先写一个简单的字典到模型的转换
先来最简单的 , 比如服务器给的数据是这种结构
// 没有嵌套字典,没有数组,都是平级的json
{
"code": 200,
"msg": "操作成功",
"success": true,
}
//字典转模型
+ (instancetype)initWithDictionary:(NSDictionary *)dic
{
id myObj = [[self alloc] init];
unsigned int outCount;
//获取类中的所有成员属性
objc_property_t *arrPropertys = class_copyPropertyList([self class], &outCount);
for (NSInteger i = 0; i < outCount; i ++) {
objc_property_t property = arrPropertys[i];
//获取属性名字符串
//model中的属性名
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
id propertyValue = dic[propertyName];
if (propertyValue != nil) {
[myObj setValue:propertyValue forKey:propertyName];
}
}
free(arrPropertys);
return myObj;
}
@end
当然这种简单的结构 , 可以不需要runtime出马 , 比如这样写,同样可以完成需求,
[self setValuesForKeysWithDictionary:dic];
这个方法也要有,防止有多给的字段
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
}
2、模型中嵌套有模型
现在难度增加 ,服务器给出了这种结构 , 字典中嵌套了字典
{
"resultMsg" : "请求成功",
"resultCode" : 200,
"isSuccess" : true,
"teacher" : {
"school" : "高中",
"job" : "老师",
"subject" : "数学",
"name" : "王XX",
"schoolId" : 1000
},
}
让model都继承rootModel, 给NSObject加类别也可以, 加类别 好处是 现有的工程几乎不用动,坏处是,以后再扩展不太方便 , 继承rootModel好处是 扩展容易 , 比较适合一个新的工程 .
- (instancetype)initWithDic:(NSDictionary *)dic {
self = [super init];
if (self) {
// [self setValuesForKeysWithDictionary:dic];
//得到当前class的所有属性
uint count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
//循环并用KVC得到每个属性的值
for (int i = 0; i<count; i++) {
objc_property_t property = properties[i];
NSString *name = @(property_getName(property));
id value = [dic valueForKey:name];
NSString *type = @(property_getAttributes(property));
NSLog(@"name = %@ type = %@ value = %@",name,type,value);
// 判断是否这个属性是RootModel的子类 , 如果是 递归的调用赋值
if ([type containsString:@"\""]) {
NSString * className = [type componentsSeparatedByString:@"\""][1];
Class modelClass = NSClassFromString(className) ;
BOOL isRootModelClass = [modelClass isKindOfClass: object_getClass([RootModel class])] ;
if (isRootModelClass) {
RootModel * subModel = [[modelClass alloc]initWithDic:value] ;
[self setValue:subModel forKey:name];
continue ;
}
}
[self setValue:value forKey:name];
}
//释放
free(properties);
}
return self;
}
重点说下property_getAttributes 函数
这个函数将返回属性(Property)的名字,@encode 编码,以及其它特征(Attribute)。比如 打印出来的type 有这样的 T@"NSString",C,N,V_name
type的字符串含义解释 :
- T@“NSNumber” 标记了属于什么类型, 以字母 T 开始,接着是@encode 编码和逗号
- N 表示属性是 nonmatic; 没有则表示是automatic
- R 不可变,如果有R, 表示属性是readonly修饰的; 没有R,则是readwrite
- C 表示属性是copy修饰
- & 表示属性是strong修饰
- W 表示属性中weak
- 如果没有用 copy / strong / weak, 那么默认的就是使用assign修饰
- 如果属性定义有定制的 getter 和 setter 方法,则字符串中有 G 或者 S 跟着相应的方法名以及逗号(例如,GcustomGetter,ScustomSetter:,,)。
如果属性是只读的,且有定制的 get 访问方法,则描述到此为止。
比如我写的一个属性的setter, type描述变成了
@property (nonatomic,setter=setNNNNName:, copy) NSString * name ;T@"NSString",C,N,SsetNNNNName:,V_name - V_name , 一般情况下字符串以 (V + 属性的名字) 结束。 _name就是变量名
官方文档地址: Declared Properties
常见的类型和对应的简写:
使用@encode(类型)可以获取该类型对应的简写,常见类型比如
v -> void
i -> int
f -> float
d -> double
B -> Bool
@ -> 对象类型,id
: -> 方法SEL
下面找了MJ上对应的简写和类型:
通过对type进行处理就可以获得属性的类型。从而进行下一步处理。
注意点:class_copyPropertyList返回的仅仅是对象类的属性,class_copyIvarList返回类的所有属性和变量,比如只有变量没有用property修饰的 是无法通过class_copyPropertyList返回的。
3、处理模型中有数组属性的情况
{
"resultMsg" : "请求成功",
"resultCode" : 200,
"isSuccess" : 1,
"resultInfo" : [
{
"name" : "用户1",
"job" : "卖萌"
},
{
"name" : "用户2",
"job" : "卖苹果"
},
{
"name" : "用户1",
"job" : "卖香蕉"
}
],
}
属性写的时候标记好RootModel的类型 , 但是 获取到的类型只有 T@“NSArray” , 这种方式无效 . 只要换一种方式了 .
@property (nonatomic,strong) NSArray <Person *> * resultInfo ;
需要一个字典来告诉RootModel , 这个json中的数组里面的子类的名字 , 于是计上心头 , 写一个类属性 , 来做这个映射, RootModel中是一个空字典 , 到真正需要映射的子类中重写这个方法 , key就是json中key , value就是RootModel的子类名
RootModel.h
/// json中嵌套了数组,数组中是一个个的RootModel子类,需要重写这个,把json中的key和model的子类名字映射
@property (nonatomic,readonly,class,strong) NSDictionary * arraryType ;
RootModel.m
// 这里上面都没做,主要防止崩溃的,真正的映射工作协作具体的子类中
+ (NSDictionary *)arraryType{
return @{};
}
// 某个子类 , json中的key和model类型映射 , PersonModel是RootModel的子类
+ (NSDictionary *)arraryType{
return @{
@"resultInfo":@"PersonModel"
};
}
然后增加了一块来处理这个NSArray
- (instancetype)initWithDic:(NSDictionary *)dic {
self = [super init];
if (self) {
// 方式1 , 简单,但是不能处理嵌套
// [self setValuesForKeysWithDictionary:dic];
// 方式2 ,
//得到当前class的所有属性
uint count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
//循环并用KVC得到每个属性的值
for (int i = 0; i<count; i++) {
objc_property_t property = properties[i];
NSString *name = @(property_getName(property));
id value = [dic valueForKey:name];
NSString *type = @(property_getAttributes(property));
NSLog(@"name = %@ type = %@ value = %@",name,type,value);
if ([type containsString:@"\""]) {
NSString * className = [type componentsSeparatedByString:@"\""][1];
// 处理model中嵌套model
Class modelClass = NSClassFromString(className) ;
BOOL isRootModelClass = [modelClass isKindOfClass: object_getClass([RootModel class])] ;
if (isRootModelClass) {
RootModel * subModel = [[modelClass alloc]initWithDic:value] ;
[self setValue:subModel forKey:name];
continue ;
}
// 处理model中嵌套数组
if ([className isEqualToString:@"NSArray"]) {
NSString * modelClassName = [[self class] jsonArraryType][name] ;
modelClass = NSClassFromString(modelClassName) ;
BOOL isRootModelClass = [modelClass isKindOfClass: object_getClass([RootModel class])] ;
if (isRootModelClass) {
NSArray * subModelArray = [modelClass modelArrayWithDicArray:value] ;
[self setValue:subModelArray forKey:name];
continue ;
}
}
}
[self setValue:value forKey:name];
}
//释放
free(properties);
}
return self;
}
4、字典中包含一些iOS不能用的字段
首先,尽量让服务器的命名规范一点, 然后直接使用json的名字作为属性的名字 ,这是最好的情况 , 但是往往天不遂人愿 , 接手一个旧的项目,服务器返回的key就叫id, 虽然我们也可以用id接受,但是用起来总是怪怪的,参照第三步的思路 ,
RootModel.h中
/// 属性名字映射到json中的Key , 比如属性的名字叫userID , 服务返回叫id , @{@"userID":@"id"}
@property (nonatomic,readonly,class,strong) NSDictionary * propertyNameToJsonKey ;
RootModel.m中
+ (NSDictionary *)propertyNameToJsonKey{
return @{};
}
RootModel的子类进行重写 , key:属性名字 , value:json中的原始名字
+ (NSDictionary *)propertyNameToJsonKey{
return @{
@"isSuccess":@"success"
};
}
- (instancetype)initWithDic:(NSDictionary *)dic {
self = [super init];
if (self) {
// 方式1 , 简单,但是不能处理嵌套
// [self setValuesForKeysWithDictionary:dic];
// 方式2 ,
//得到当前class的所有属性
uint count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
//循环并用KVC得到每个属性的值
for (int i = 0; i<count; i++) {
objc_property_t property = properties[i];
NSString *propertyName = @(property_getName(property));
id value = [dic valueForKey:propertyName];
// 处理value , 如果propertyNameToJsonKey有值,
NSString * jsonKey = [[self class]propertyNameToJsonKey][propertyName] ;
if (jsonKey != nil) {
value = [dic valueForKey:jsonKey];
}
NSString *type = @(property_getAttributes(property));
NSLog(@"name = %@ type = %@ value = %@",propertyName,type,value);
if ([type containsString:@"\""]) {
NSString * className = [type componentsSeparatedByString:@"\""][1];
// 处理model中嵌套model
Class modelClass = NSClassFromString(className) ;
BOOL isRootModelClass = [modelClass isKindOfClass: object_getClass([RootModel class])] ;
if (isRootModelClass) {
RootModel * subModel = [[modelClass alloc]initWithDic:value] ;
[self setValue:subModel forKey:propertyName];
continue ;
}
// 处理model中嵌套数组
if ([className isEqualToString:@"NSArray"]) {
NSString * modelClassName = [[self class] jsonArraryType][propertyName] ;
modelClass = NSClassFromString(modelClassName) ;
BOOL isRootModelClass = [modelClass isKindOfClass: object_getClass([RootModel class])] ;
if (isRootModelClass) {
NSArray * subModelArray = [modelClass modelArrayWithDicArray:value] ;
[self setValue:subModelArray forKey:propertyName];
continue ;
}
}
}
[self setValue:value forKey:propertyName];
}
//释放
free(properties);
}
return self;
}
此处有个小问题, 就是 propertyNameToJsonKey 只会取当前类重写的方法, 没有取到父类的方法, 此处应该构造一个新的字典, 然后从当前类到NSObject中的所有返回值都加入到新字典中.上面获取jsonKey的方式改成这样.
// 处理value , 如果propertyNameToJsonKey有值,
NSDictionary *nameToJsonDic = [self p_allPropertyNameToJsonKey];
NSString * jsonKey = nameToJsonDic[propertyName] ;
if (jsonKey != nil) {
value = [dic valueForKey:jsonKey];
}
/// 获取从当前类开始的到基类的propertyNameToJsonKey实现
- (NSDictionary *)p_allPropertyNameToJsonKey {
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
Class cls = [self class];
while (cls) {
if ([cls respondsToSelector:@selector(propertyNameToJsonKey)]) {
[dic addEntriesFromDictionary:[cls propertyNameToJsonKey]];
}
cls = [cls superclass];
}
return [dic copy];
}
5、存在model的继承
还有一种情况就是model的继承关系处理, 比如说一个PersonModel继承自RootModel, 然后又有一个TeacherModel继承了PersonModel, 由于class_copyPropertyList([self class], &count) 只是返回了当前类的属性, 父类的属性是没有返回的, 所以想要父类的model也转换成功, 就需要从[self class]往上找,直到找到NSObject.
就仿照系统的class_copyPropertyList实现了一个自己的方法, 此方法可以获取从本类开始的所有父类的属性列表, 然后依然返回objc_property_t数组
/// 把系统的C语言方法扩展下, 支持获取父类的属性
//objc_property_t *class_copyPropertyList(Class cls, unsigned int * outCount)
objc_property_t* gcs_class_copyPropertyList(Class cls, unsigned int * outCount) {
// 可惜自己C语言内存管理的不了解,下面的做法有点傻,应该有办法通过一次遍历完成的
// 1.先统计出来有多少属性,过滤掉了NSObject的属性,因为NSObject中有很多无效的属性,
// 2.然后一次性的malloc对应大小的数组,
// 3.在遍历一次把objc_property_t加入到数组中
/*
NSObject中的无效属性举例:
hash
superclass
description
debugDescription
*/
// 1.只能先统计出来有多少属性,
unsigned int allCount = 0;
Class tempCls = cls;
while (tempCls != [NSObject class]) {
uint count;
objc_property_t *properties = class_copyPropertyList(tempCls, &count);
allCount += count;
tempCls = [tempCls superclass];
free(properties);
}
// 2.然后一次性的malloc对应大小的数组,
objc_property_t *allProperties = malloc(sizeof(objc_property_t) * allCount);
// 3.在遍历一次把objc_property_t加入到数组中
tempCls = cls;
int j = 0;
while (tempCls != [NSObject class]) {
uint count;
objc_property_t *properties = class_copyPropertyList(tempCls, &count);
//循环并用KVC得到每个属性的值
for (int i = 0; i<count; i++) {
objc_property_t property = properties[i];
NSString *propertyName = @(property_getName(property));
NSLog(@"属性名字: %@ 对应类: %@",propertyName,tempCls);
allProperties[j] = property;
j++;
}
free(properties);
tempCls = [tempCls superclass];
}
*outCount = allCount;
return allProperties;
}
有了自己的方法后, 只需要把以前调用 class_copyPropertyList -> gcs_class_copyPropertyList, 由于返回值和入参完全一样, 其他地方的就完成不需要修改了.
6 . 舒服的打印RootModel
但是还有一点 , 直接打印log 的话,信息很少 , 看起来不舒服 , 所以重写RootModel的description,是日志的数据更舒服点.
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p> -- %@",[self class],self,[self modelToDictionary]];
}
/// 把一个RootModel还原成成一个字典 , 主要为了description使用
- (NSDictionary *)modelToDictionary {
//初始化一个字典
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
//得到当前class的所有属性
uint count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
//循环并用KVC得到每个属性的值
for (int i = 0; i<count; i++) {
objc_property_t property = properties[i];
NSString *name = @(property_getName(property));
id value = [self valueForKey:name]?:@"nil";//默认值为nil字符串
// model中嵌套了子model
if ([value isKindOfClass:[RootModel class]]) {
RootModel * subModel = value ;
//递归调用子model , 装载到字典里
[dictionary setObject:[subModel modelToDictionary] forKey:name];
continue ;
}
// model中嵌套了array , array中都是RootModel
if ([value isKindOfClass:[NSArray class]]) {
NSArray * subArray = value ;
NSMutableArray * temp = [[NSMutableArray alloc]initWithCapacity:subArray.count];
for (RootModel * subModel in subArray) {
[temp addObject:[subModel modelToDictionary]];
}
[dictionary setObject:temp forKey:name];
continue ;
}
//普通属性装载到字典里
[dictionary setObject:value forKey:name];
}
//释放
free(properties);
//return
return dictionary;
}
好了 , 完成 , 博客排版不好 , ,想要看的舒服 , 嘿嘿.
如果想在你的工程中用 , 直接找到RootModel.h 和 .m文件,拖入到工程就行了