前言

我们在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就是变量名

ios 本地json数据修改 ios json转model_json

官方文档地址: Declared Properties 

常见的类型和对应的简写:

使用@encode(类型)可以获取该类型对应的简写,常见类型比如
v  ->  void
i  ->  int
f  ->  float
d  ->  double
B  ->  Bool
@  ->  对象类型,id
:  ->  方法SEL

下面找了MJ上对应的简写和类型:

ios 本地json数据修改 ios json转model_ios_02

 通过对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

ios 本地json数据修改 ios json转model_json_03

- (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"
             };
}

ios 本地json数据修改 ios json转model_嵌套_04

- (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文件,拖入到工程就行了