Runtime实现iOS字典转模型

在开发中,对于处理网络请求中获取的数据(即把请求到的json或字典转换成方便使用的数据模型)是我们在开发中必不可少的操作。诸如强大的第三方MJExtensionJSONModel或者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 <objc/runtime.h>

@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 <objc/runtime.h>。其中的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 <objc/runtime.h>

@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 <objc/runtime.h>

@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:<People: 0x604000259f20>
2018-08-08 11:21:36.198389+0800 WXQModel_Runtime[2488:99552] Lesson:<Lesson: 0x604000230560>
2018-08-08 11:21:36.198533+0800 WXQModel_Runtime[2488:99552] School:<School: 0x6040002301a0>
2018-08-08 11:21:36.198733+0800 WXQModel_Runtime[2488:99552] Grade:<Grade: 0x604000230580>
2018-08-08 11:21:36.199065+0800 WXQModel_Runtime[2488:99552] teacher:Mr Li

从结果中发现我们并不能直接打印出模型类中的所有属性值,这就需要我们去重写所有模型类的- (NSString *)description;方法。创建一个父类@interface BaseModel : NSObject,导入头文件#import <objc/message.h>,重写description方法,如下:

#import "BaseModel.h"
#import <objc/message.h>

@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:<People: 0x604000259ce0>:[ 
    age = 18
    name = Xiaoming
    sex = 男
    school = <School: 0x60400003c2e0>:[ 
    name = 海淀一中
    address = 海淀区
    grade = <Grade: 0x60400022f260>:[ 
    name = 九年级
    teacher = Mr Li
]
]
    lessons = (
    "<Lesson: 0x600000422a00>:[ \n\tname = \U8bed\U6587\n\tscore = 125\n]",
    "<Lesson: 0x60400022f1a0>:[ \n\tname = \U6570\U5b66\n\tscore = 146\n]",
    "<Lesson: 0x60400022f0e0>:[ \n\tname = \U82f1\U8bed\n\tscore = 112\n]"
)
]
2018-08-08 11:27:55.873911+0800 WXQModel_Runtime[2610:105369] Lesson:<Lesson: 0x60400022f0e0>:[ 
    name = 英语
    score = 112
]
2018-08-08 11:27:55.874120+0800 WXQModel_Runtime[2610:105369] School:<School: 0x60400003c2e0>:[ 
    name = 海淀一中
    address = 海淀区
    grade = <Grade: 0x60400022f260>:[ 
    name = 九年级
    teacher = Mr Li
]
]
2018-08-08 11:27:55.874280+0800 WXQModel_Runtime[2610:105369] Grade:<Grade: 0x60400022f260>:[ 
    name = 九年级
    teacher = Mr Li
]
2018-08-08 11:27:55.874462+0800 WXQModel_Runtime[2610:105369] teacher:Mr Li

欢迎指正交流0_0~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容

  • 引导 对于从事 iOS 开发人员来说,所有的人都会答出「 Runtime 是运行时 」,什么情况下用 Runtim...
    Winny_园球阅读 4,150评论 3 75
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,027评论 8 265
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,674评论 7 64
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,037评论 1 32
  • 很多企业在做Google PPC (Pay Per Click)推广的时候,都没有仔细研究到用户真正会搜索的关键字...
    印姐说说阅读 175评论 0 0