runtime模型与字典互转

前言

在开发中必不可少的模型与字典互转,但是一直以来都是使用他人的库,从来没有研究其原理或者说深究其所以然。现在,在这里我们一起来学习通过runtime完成模型与字典的互转。

声明Model

在开始介绍详细API之前,我们先来声明一个模型类TestModel,这个类提供了根据字典转换成模型类对象的功能,还提供了将模型类转换成字典的功能:

//
//  TestModel.h
//  RuntimeDemo
//
//  Copyright © 2017年 . All rights reserved.
//

#import <Foundation/Foundation.h>

@protocol EmptyPropertyProperty <NSObject>

// 设置默认值,若为空,则取出来的就是默认值
- (NSDictionary *)defaultValueForEmptyProperty;

@end

@interface TestModel : NSObject <EmptyPropertyProperty>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSNumber *count;
@property (nonatomic, assign) int    commentCount;
@property (nonatomic, strong) NSArray *summaries;
@property (nonatomic, strong) NSDictionary *parameters;
@property (nonatomic, strong) NSSet *results;

@property (nonatomic, strong) TestModel *testModel;

// 只读属性
@property (nonatomic, assign, readonly) NSString *classVersion;

// 通过这个方法来实现自动生成model
- (instancetype)initWithDictionary:(NSDictionary *)dictionary;

// 转换成字典
- (NSDictionary *)toDictionary;

// 测试
+ (void)test;

@end

我们这里声明了几种类型,主要是模型中的数组、字典、集合、对象属性,我们最后要转换对象成字典。

实现代码

在我们讲解之前,先把代码全部放出来,我们再一一讲解:

#import "TestModel.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation TestModel

- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
  if (self = [super init]) {
    for (NSString *key in dictionary.allKeys) {
      id value = [dictionary objectForKey:key];
      
      if ([key isEqualToString:@"testModel"]) {
        TestModel *testModel = [[TestModel alloc] initWithDictionary:value];
        value = testModel;
        self.testModel = testModel;
        
        continue;
      }
      
      SEL setter = [self propertySetterWithKey:key];
      if (setter != nil) {
        ((void (*)(id, SEL, id))objc_msgSend)(self, setter, value);
      }
    }
  }
  
  return self;
}

- (NSDictionary *)toDictionary {
  unsigned int outCount = 0;
  objc_property_t *properties = class_copyPropertyList([self class], &outCount);
  
  if (outCount != 0) {
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:outCount];
    
    for (unsigned int i = 0; i < outCount; ++i) {
      objc_property_t property = properties[i];
      const void *propertyName = property_getName(property);
      NSString *key = [NSString stringWithUTF8String:propertyName];
      
      // 继承于NSObject的类都会有这几个在NSObject中的属性
      if ([key isEqualToString:@"description"]
          || [key isEqualToString:@"debugDescription"]
          || [key isEqualToString:@"hash"]
          || [key isEqualToString:@"superclass"]) {
        continue;
      }
      
      // 我们只是测试,不做通用封装,因此这里不额外写方法做通用处理,只是写死测试一下效果
      if ([key isEqualToString:@"testModel"]) {
        if ([self respondsToSelector:@selector(toDictionary)]) {
          id testModel = [self.testModel toDictionary];
          if (testModel != nil) {
            [dict setObject:testModel forKey:key];
          }
          continue;
        }
      }
      
      SEL getter = [self propertyGetterWithKey:key];
      if (getter != nil) {
        // 获取方法的签名
        NSMethodSignature *signature = [self methodSignatureForSelector:getter];
        
        // 根据方法签名获取NSInvocation对象
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        // 设置target
        [invocation setTarget:self];
        // 设置selector
        [invocation setSelector:getter];
        
        // 方法调用
        [invocation invoke];
        
        // 接收返回的值
        __unsafe_unretained NSObject *propertyValue = nil;
        [invocation getReturnValue:&propertyValue];
        
        //        id propertyValue = [self performSelector:getter];
        
        if (propertyValue == nil) {
          if ([self respondsToSelector:@selector(defaultValueForEmptyProperty)]) {
            NSDictionary *defaultValueDict = [self defaultValueForEmptyProperty];
            
            id defaultValue = [defaultValueDict objectForKey:key];
            propertyValue = defaultValue;
          }
        }
        
        if (propertyValue != nil) {
          [dict setObject:propertyValue forKey:key];
        }
      }
    }
    
    free(properties);
    
    return dict;
  }
  
  free(properties);
  return nil;
}

- (SEL)propertyGetterWithKey:(NSString *)key {
  if (key != nil) {
    SEL getter = NSSelectorFromString(key);
    
    if ([self respondsToSelector:getter]) {
      return getter;
    }
  }
  
  return nil;
}

- (SEL)propertySetterWithKey:(NSString *)key {
  NSString *propertySetter = key.capitalizedString;
  propertySetter = [NSString stringWithFormat:@"set%@:", propertySetter];
  
  // 生成setter方法
  SEL setter = NSSelectorFromString(propertySetter);
  
  if ([self respondsToSelector:setter]) {
    return setter;
  }
  
  return nil;
}

#pragma mark - EmptyPropertyProperty
- (NSDictionary *)defaultValueForEmptyProperty {
  return @{@"name" : [NSNull null],
           @"title" : [NSNull null],
           @"count" : @(1),
           @"commentCount" : @(1),
           @"classVersion" : @"0.0.1"};
}

+ (void)test {
  NSMutableSet *set = [NSMutableSet setWithArray:@[@"可变集合", @"字典->不可变集合->可变集合"]];
  NSDictionary *dict = @{@"name"  : @"技术博客",
                         @"title" : @"http://www.henishuo.com",
                         @"count" : @(11),
                         @"results" : [NSSet setWithObjects:@"集合值1", @"集合值2", set , nil],
                         @"summaries" : @[@"sm1", @"sm2", @{@"keysm": @{@"stkey": @"字典->数组->字典->字典"}}],
                         @"parameters" : @{@"key1" : @"value1", @"key2": @{@"key11" : @"value11", @"key12" : @[@"三层", @"字典->字典->数组"]}},
                         @"classVersion" : @(1.1),
                         @"testModel" : @{@"name"  : @"技术博客",
                                          @"title" : @"http://www.henishuo.com",
                                          @"count" : @(11),
                                          @"results" : [NSSet setWithObjects:@"集合值1", @"集合值2", set , nil],
                                          @"summaries" : @[@"sm1", @"sm2", @{@"keysm": @{@"stkey": @"字典->数组->字典->字典"}}],
                                          @"parameters" : @{@"key1" : @"value1", @"key2": @{@"key11" : @"value11", @"key12" : @[@"三层", @"字典->字典->数组"]}},
                                          @"classVersion" : @(1.1)}};
  TestModel *model = [[TestModel alloc] initWithDictionary:dict];
  
  NSLog(@"%@", model);
  
  NSLog(@"model->dict: %@", [model toDictionary]);
}

@end

细节讲解

注意到这一行代码了吗?这是将对应的值赋值给对应的属性:

((void (*)(id, SEL, id))objc_msgSend)(self, setter, value)

我们需要明确一点,objc_msgSend函数是用于发送消息的,而这个函数是可以很多个参数的,但是我们必须手动转换成对应类型参数,比如上面我们就是强制转换objc_msgSend函数类型为带三个参数且返回值为void函数,然后才能传三个参数。如果我们直接通过objc_msgSend(self, setter, value)是报错,说参数过多。

(void (*)(id, SEL, id)这是C语言中的函数指针,如果不了解C,没有关系,我们只需要记住参数列表前面是一个(*)这样的就是对应函数指针了。

生成Setter方法

我们要通过objc_msgSend函数来发送消息给对象,然后通过属性的setter方法来赋值,那么我们要生成setter选择器:

- (SEL)propertySetterWithKey:(NSString *)key {
  NSString *propertySetter = key.capitalizedString;
  propertySetter = [NSString stringWithFormat:@"set%@:", propertySetter];
  
  // 生成setter方法
  SEL setter = NSSelectorFromString(propertySetter);
  
  if ([self respondsToSelector:setter]) {
    return setter;
  }
  
  return nil;
}

这里就是生成属性的setter选择器。我们知道,系统生成属性的setter方法的规范是setKey:这样的格式,因此我们只要按照同样的规则生成setter就可以了。另外我们还需要判断是否可以响应此setter方法。

模型中有模型属性

对于本demo中,模型中还有模型属性,那么我们应该如何来赋值呢?其实,我们需要注意一点,一定要给模型属性分配内存,否则看起来赋值了,但是对象还是空。

if ([key isEqualToString:@"testModel"]) {
    TestModel *testModel = [[TestModel alloc] initWithDictionary:value];
    value = testModel;
    self.testModel = testModel;
        
    continue;
}

这里是在for循环中的,我们一定要注意加上continue,否则下边可能会将其值设置为空哦。

生成Getter方法

我们可以通过NSSelectorFromString函数来生成SEL选择器,当然我们也可以通过@selector()生成SEL选择器,但是我们这里只能使用前者:

- (SEL)propertyGetterWithKey:(NSString *)key {
  if (key != nil) {
    SEL getter = NSSelectorFromString(key);
    
    if ([self respondsToSelector:getter]) {
      return getter;
    }
  }
  
  return nil;
}

模型转字典

首先,我们需要先获取所有的属性,以便获取属性值:

unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);

objc_property_t类型代表属性,它是一个结构体。通过函数class_copyPropertyList可以获取对象的属性列表及属性的个数。

在遍历属性列表时,我们通过这样获取名称:

objc_property_t property = properties[i];
const void *propertyName = property_getName(property);
NSString *key = [NSString stringWithUTF8String:propertyName];

由于继承于NSObject的对象,都有这几个属性,因此我们需要过滤掉:

// 继承于NSObject的类都会有这几个在NSObject中的属性
if ([key isEqualToString:@"description"]
  || [key isEqualToString:@"debugDescription"]
  || [key isEqualToString:@"hash"]
  || [key isEqualToString:@"superclass"]) {
    continue;
}

调用getter方法获取值

我们要通过runtime获取值,常用的有两种方式:

  • 通过performSelector方法
  • 通过NSInvocation对象

下面是通过NSInvocation的方法,流程可以这样:先获取方法签名,然后根据方法签名生成NSInvocation对象,设置targetSEL,然后调用,最后获取返回值:

SEL getter = [self propertyGetterWithKey:key];
if (getter != nil) {
  // 获取方法的签名
  NSMethodSignature *signature = [self methodSignatureForSelector:getter];
  
  // 根据方法签名获取NSInvocation对象
  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
  // 设置target
  [invocation setTarget:self];
  // 设置selector
  [invocation setSelector:getter];
  
  // 方法调用
  [invocation invoke];
  
  // 接收返回的值
  __unsafe_unretained NSObject *propertyValue = nil;
  [invocation getReturnValue:&propertyValue];
  
  //        id propertyValue = [self performSelector:getter];
}

如果是通过performSelector方式,一行代码就可以了,但是会提示有内存泄露的风险,通常使用上面这种方式,而不是直接使用下面这种方式:

id propertyValue = [self performSelector:getter];

我们这里还做了额外的处理,当属性值获取到是空的时候,我们可以通过协议指定默认值。当值为空时,我们就会使用默认值:

if (propertyValue == nil) {
  if ([self respondsToSelector:@selector(defaultValueForEmptyProperty)]) {
    NSDictionary *defaultValueDict = [self defaultValueForEmptyProperty];
    
    id defaultValue = [defaultValueDict objectForKey:key];
    propertyValue = defaultValue;
  }
}

模型属性转换成字典

我们这里的模型有一个属性也是一个模型,因此我们需要额外处理一下:

// 我们只是测试,不做通用封装,因此这里不额外写方法做通用处理,只是写死测试一下效果
if ([key isEqualToString:@"testModel"]) {
  if ([self respondsToSelector:@selector(toDictionary)]) {
    id testModel = [self.testModel toDictionary];
    if (testModel != nil) {
      [dict setObject:testModel forKey:key];
    }
    continue;
  }
}

注意,这里是写死的哦,因为我们这里写死了testModel,只是为了简化,如果要封装成通用的方法,那么就需要做更多的工作了。不过我们这里的目的是学习如何转,而不是封装成能用的库。因此,研究明白其原理才是目的。

测试

我们测试一下这样复杂的结构:

+ (void)test {
  NSMutableSet *set = [NSMutableSet setWithArray:@[@"可变集合", @"字典->不可变集合->可变集合"]];
  NSDictionary *dict = @{@"name"  : @"技术博客",
                         @"title" : @"http://www.henishuo.com",
                         @"count" : @(11),
                         @"results" : [NSSet setWithObjects:@"集合值1", @"集合值2", set , nil],
                         @"summaries" : @[@"sm1", @"sm2", @{@"keysm": @{@"stkey": @"字典->数组->字典->字典"}}],
                         @"parameters" : @{@"key1" : @"value1", @"key2": @{@"key11" : @"value11", @"key12" : @[@"三层", @"字典->字典->数组"]}},
                         @"classVersion" : @(1.1),
                         @"testModel" : @{@"name"  : @"技术博客",
                                          @"title" : @"http://www.henishuo.com",
                                          @"count" : @(11),
                                          @"results" : [NSSet setWithObjects:@"集合值1", @"集合值2", set , nil],
                                          @"summaries" : @[@"sm1", @"sm2", @{@"keysm": @{@"stkey": @"字典->数组->字典->字典"}}],
                                          @"parameters" : @{@"key1" : @"value1", @"key2": @{@"key11" : @"value11", @"key12" : @[@"三层", @"字典->字典->数组"]}},
                                          @"classVersion" : @(1.1)}};
  TestModel *model = [[TestModel alloc] initWithDictionary:dict];
  
  NSLog(@"%@", model);
  
  NSLog(@"model->dict: %@", [model toDictionary]);
}

打印效果

模型转字典的效果:

2017-12-29 16:02:15.207 RuntimeDemo[40233:1083396] model->dict:     {
    classVersion = "0.0.1",
    count = 11,
    parameters =    {
        key1 = "value1",
        key2 =  {
            key11 = "value11",
            key12 =     (
                "三层",
                "字典->字典->数组",
            ),
        },
    },
    results =   {(
        "集合值2",
        "集合值1",
            {(
            "可变集合",
            "字典->不可变集合->可变集合",
        )},
    )},
    title = "http://www.henishuo.com",
    commentCount = 1,
    testModel =     {
        classVersion = "0.0.1",
        count = 11,
        parameters =    {
            key1 = "value1",
            key2 =  {
                key11 = "value11",
                key12 =     (
                    "三层",
                    "字典->字典->数组",
                ),
            },
        },
        results =   {(
            "集合值2",
            "集合值1",
                {(
                "可变集合",
                "字典->不可变集合->可变集合",
            )},
        )},
        title = "http://www.henishuo.com",
        commentCount = 1,
        name = "技术博客",
        summaries =     (
            "sm1",
            "sm2",
                {
                keysm =     {
                    stkey = "字典->数组->字典->字典",
                },
            },
        ),
    },
    name = "技术博客",
    summaries =     (
        "sm1",
        "sm2",
            {
            keysm =     {
                stkey = "字典->数组->字典->字典",
            },
        },
    ),
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容

  • 根据前面的学习iOS开发之使用Runtime给Model类赋值和OC和Swift中的Runtime,总结一下将字典...
    John_LS阅读 2,567评论 5 1
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,709评论 0 9
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,721评论 7 64
  • 青生病了,上吐下泻,还发烧。 想来是昨天竹筏漂流受凉了,没穿雨披,寒气内袭所致。 一向结实的他,仗着身板儿硬实,梗...
    陌生如我阅读 99评论 0 0
  • 自古人生最忌满,半贫半富半自安; 半命半天半机遇,半取半舍半行善; 半聋半哑半糊涂,半智半愚半圣贤; 半人半我半自...
    半生缘_ab19阅读 156评论 0 0