利用Runtime实现简单的字典转模型

前言

我们都知道,开发中会有这么一个过程,就是将服务器返回的数据转换成我们自己定义的模型对象。当然服务器返回的数据结构有xml类型的,也有json类型的。本文只讨论json格式的。

大家在项目中一般是怎么样将服务器返回的json转化成自己定义的模型类呢?

我在项目中一般都是使用的MJExtension
本文讲解的也基本就是解读MJExtension中的部分源码。
好了,废话不多说,直接上代码,let's go

简单字典转模型

首先,从最简单的字典开始,例如我们需要将如下的字典转化成自定义的模型。

	 NSDictionary *dict = @{@"name":@"Scott",
	                        @"icon" : @"lufy.png",
	                        @"age" : @"20",
	                        @"height" : @1.75,
	                        @"money" : @"100.9",
	                        @"sex" : @(SexMale),
	                        @"gay" : @"ture",
	                        };

我们定义一个ScottUser类,并且定义好属性名如下:

	#import <Foundation/Foundation.h>
	typedef NS_ENUM(NSInteger, Sex) {
	    SexMale,    // 男
	    SexFemale   // 女
	};
	@interface ScottUser : NSObject
	/**  姓名 */
	@property (nonatomic, copy) NSString *name;
	/**  头像 */
	@property (nonatomic, copy) NSString *icon;
	/**  年龄 */
	@property (nonatomic, assign) unsigned int age;
	/**  身高 */
	@property (nonatomic, strong) NSNumber *height;
	/**  财富 */
	@property (nonatomic, copy) NSString *money;
	/**  性别 */
	@property (nonatomic, assign) Sex sex;
	/**  是否同性 */
	@property (nonatomic, assign, getter=isGay) BOOL gay;
	@end

到此为止,我们下一步的目标就是拿到字典里面的值(value)对ScottUser模型属性进行赋值,模型的属性名对应着字典里面的key

最直接的方法就是:

    ScottUser *user = [[ScottUser alloc] init];
    user.name = dict[@"name"];
    user.icon = dict[@"icon"];
    ...

但是,对于每一次的数据转模型,你都要这样去写大量的重复代码,毫无意义。
当然我们利用setValuesForKeysWithDictionary:(NSDictionary *)dict进行kvc赋值。

KVC赋值

  • 优点:不需要去手动一个一个属性赋值。
  • 缺点:当自定义的属性和字典中的key不一样的时候,会报错。
  • 解决办法:重写- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法。

我们可以通过写一个框架自动帮我们实现字典转模型,大致思路就是:

  1. 遍历模型中的属性,然后拿到属性名作为键值去字典中寻找;
  2. 找到后,根据模型的属性类型转化成正确的类型;
  3. 属性名赋值。

遍历模型中的属性,拿到属性名作为键值去字典中寻找

方法伪代码:

[模型类 遍历属性的方法];

为了方便使用,创建一个叫NSObject+ScottProperty的分类,写一个获取所有属性的方法。

	#import <Foundation/Foundation.h>
	@interface NSObject (ScottProperty)
	+ (NSArray *)properties;
	@end

假设我们在看不到一个类的.h.m文件的前提下,有什么办法可以获取它所有的实例变量呢?

答案是通过Runtime

	#import "NSObject+ScottProperty.h"
	#import <objc/runtime.h>
	@implementation NSObject (ScottProperty)
	+ (NSArray *)properties {
	    NSMutableArray *propertiesArr = [NSMutableArray array];
	    unsigned int outCount = 0;
	    objc_property_t *properties = class_copyPropertyList(self, &outCount);
	    for (int i=0; i<outCount; i++) {
	        objc_property_t property = properties[i];
	        [propertiesArr addObject:propertyObj];
	        char *name = property_getName(property);
	        char *att = property_getAttributes(property);
	        NSLog(@"name:%s-----att:%s",name,att);
	    }
	    return propertiesArr;
	}

在外部调用+ (NSArray *)properties方法能够打印出一个类的所有属性,如:

NSArray *arr = [ScottUser properties];

运行程序,能够看到控制台的输出:
图1

从输出中可以看到通过property_getName()获取每一个objc_property_tname表示成员属性的名字,通过property_getAttributes()获取每一个objc_property_tattributes表示成员属性中的一些特性(如是什么类,原子性还是非原子性,是strong还是weak还是copy,生成的成员变量名等信息...)

从苹果的官方文档(Objective-C Runtime Programming Guide)可以得知,attributes是一个类型编码字符串,这个字符串以T作为开始,接上@encode类型编码和一个逗号,以V接上实例变量名作为结尾,在他们之间是一些其他信息,以逗号分隔,具体内容可以查看官方文档中详细的表格。

在实际赋值过程中,我们并不关心该属性的内存管理、生成的成员变量名、或者其他什么信息,在attributes中,只需要知道它所属的或者知道什么基本数据类型,即T第一个逗号之前中间的内容,如果是的话还需要将@""去掉。

实际上,Runtime已经给我们提供获取属性名和属性特性的函数了,也就是通过property_getName()property_getAttributes()

这时候我们就可以获取到属性名和属性对应的属性特性了。

找到后,根据属性类型转化成正确的类型

现在已经完成了第一步,并且拿到了属性名,但是数据类型还需要我们进一步截取,截取方法如下:

	for (int i=0; i<outCount; i++) {
        objc_property_t property = properties[i];
        // 为了以后方便使用,将C字符串转化成OC对象
	//    char *name = property_getName(property);
	    NSString *name = @(property_getName(property));
	//    char *att = property_getAttributes(property);
	    NSString *att = @(property_getAttributes(property));
        NSUInteger loc = 1;
        NSUInteger len = [att rangeOfString:@","].location - loc;
        NSString *type = [att substringWithRange:NSMakeRange(loc, len)];
        NSLog(@"%@",type);
    }

控制台结果显示,我们能够截取到其中的类型了:
图2

回归到我们拿到这些属性类型的初衷,是为了用字典中的值的类型与模型中属性的类型进行对比,想要对比,需要拿到属性的类型,因此需要将这些编码转换成一个表示类的类,创建一个类用来包装类型。

	#import <Foundation/Foundation.h>
	@interface ScottPropertyType : NSObject
	/**  是否为id类型 */
	@property (nonatomic, readonly, getter=isIdType) BOOL idType;
	/**  是否为基本数据类型(int、float等) */
	@property (nonatomic, readonly, getter=isNumberType) BOOL numberType;
	/**  是否为bool类型 */
	@property (nonatomic, readonly, getter=isBoolType) BOOL boolType;
	/**  对象类型(如果是基本数据类型,此值为nil) */
	@property (nonatomic, readonly) Class typeClass;
	@end

OC对象可以通过Class来表示类型,而基本数据类型只能用布尔来标识。

把这些名字和类型遍历出来,肯定是为了以后有用,所以需要把它们存起来,由于它们是一个"整体",所以还是设计一个类将他们包装起来比较好,创建一个包装成员属性的类--ScottProperty

	#import <Foundation/Foundation.h>
	@class ScottPropertyType;
	@interface ScottProperty : NSObject
	/**  属性名 */
	@property (nonatomic, readonly) NSString *name;
	/**  成员属性的类型 */
	@property (nonatomic, readonly) ScottPropertyType *type;
	@end

这时,代码就可以进行重构了,将属于不同类的功能封装到对应的类上,让ScottProperty提供一个类方法用于返回一个将objc_property_t进行包装的类。

	 for (int i=0; i<outCount; i++) {
	        objc_property_t property = properties[i];
	        ScottProperty *propertyObj = [ScottProperty propertyWithProperty:property];
	        [propertiesArr addObject:propertyObj];
	    }

propertyWithProperty:方法实现如下:

	@implementation ScottProperty
	+ (instancetype)propertyWithProperty:(objc_property_t)property {
	    return [[ScottProperty alloc] initWithProperty:property];
	}
	- (instancetype)initWithProperty:(objc_property_t)property {
	    if (self = [super init]) {
	        _name = @(property_getName(property));
	        _type = [ScottPropertyType propertiesWithAttributeString:@(property_getAttributes(property))];
	    }
	    return self;
	}
	@end

ScottPropertyType也提供类方法用于包装类型:

	#import "ScottPropertyType.h"
	@implementation ScottPropertyType
	+ (instancetype)propertiesWithAttributeString:(NSString *)att {
	    return [[ScottPropertyType alloc] initWithTypeString:att];
	}
	- (instancetype)initWithTypeString:(NSString *)typeString {   
	    if (self = [super init]) {
	        NSUInteger loc = 1;
	        NSUInteger len = [typeString rangeOfString:@","].location - loc;
	        NSString *typeCode = [typeString substringWithRange:NSMakeRange(loc, len)];
	        NSLog(@"%@",typeCode);
	    }
	    return self;
	}
	@end

重构完成之后,结构显得更加清晰,更加有利于接下来的工作,下面继续完成typeCode的提取。
运行重构之后的代码,可以看到和重构之前是一样的:
图3

上面提到的这些类型,是类型编码,在苹果文档中告诉我们编码对应的类型:
图4
根据这个对应关系的图表,我们将常用的几个编码定义成常量字符串或者宏表示它所对应的类型,利于编码和阅读:
ScottPropertyType类定义以下属性类型:

	/**
	 *  成员变量类型(属性类型)
	 */
	NSString *const ScottPropertyTypeInt = @"i";
	NSString *const ScottPropertyTypeShort = @"s";
	NSString *const ScottPropertyTypeFloat = @"f";
	NSString *const ScottPropertyTypeDouble = @"d";
	NSString *const ScottPropertyTypeLong = @"q";
	NSString *const ScottPropertyTypeChar = @"c";
	NSString *const ScottPropertyTypeBOOL1 = @"c";
	NSString *const ScottPropertyTypeBOOL2 = @"b";
	NSString *const ScottPropertyTypePointer = @"*";
	NSString *const ScottPropertyTypeIvar = @"^{objc_ivar=}";
	NSString *const ScottPropertyTypeMethod = @"^{objc_method=}";
	NSString *const ScottPropertyTypeBlock = @"@?";
	NSString *const ScottPropertyTypeClass = @"#";
	NSString *const ScottPropertyTypeSEL = @":";
	NSString *const ScottPropertyTypeId = @"@";

并写一个方法用于提取每个属性的类型:

	- (instancetype)initWithTypeString:(NSString *)typeString {
	    if (self = [super init]) {
	        NSUInteger loc = 1;
	        NSUInteger len = [typeString rangeOfString:@","].location - loc;
	        NSString *typeCode = [typeString substringWithRange:NSMakeRange(loc, len)];
	        [self getTypeCode:typeCode];
	    }
	    return self;
	}
	- (void)getTypeCode:(NSString *)code {
	    if ([code isEqualToString:ScottPropertyTypeId]) {
	        _idType = YES;
	    }else if (code.length > 3 && [code hasPrefix:@"@\""]){
	        // 去掉@"和",截取中间的类型名称
	        code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
	        _typeClass = NSClassFromString(code);
	        _numberType = (_typeClass == [NSNumber class] || [_typeClass isSubclassOfClass:[NSNumber class]]);
	    }
	    // 是否为数字类型
	    NSString *lowerCode = code.lowercaseString;
	    NSArray *numberTypes = @[ScottPropertyTypeInt,
	                             ScottPropertyTypeShort,
	                             ScottPropertyTypeFloat,
	                             ScottPropertyTypeDouble,
	                             ScottPropertyTypeLong,
	                             ScottPropertyTypeChar,
	                             ScottPropertyTypeBOOL1,
	                             ScottPropertyTypeBOOL2];
	    if ([numberTypes containsObject:lowerCode]) {
	        _numberType = YES;
	        if ([lowerCode isEqualToString:ScottPropertyTypeBOOL1] || [lowerCode isEqualToString:ScottPropertyTypeBOOL2]) {
	            _boolType = YES;
	        }
	    }
	}

到这里,我们一个ScottProperty的骨架大致就搭好了。

NSObject+ScottProperty分类中遍历属性的时候,打印属性名和属性类型看看:

	for (int i=0; i<outCount; i++) {
	        objc_property_t property = properties[i];
	        ScottProperty *propertyObj = [ScottProperty propertyWithProperty:property];
	        [propertiesArr addObject:propertyObj];
	        NSLog(@"name:%@--type:%@",propertyObj.name,propertyObj.type.typeClass);
	    }

图5
从图中可以看出,属于基本类型的属性打印出来的类型是null,其他的都能正确打印出对应类型。

当我们想要使用字典转模型功能的时候,提供一个类方法方便转换,该方法放在NSObject+ScottKeyValue分类中,该分类负责字典转模型的方法实现。

	+ (instancetype)objectWithKeyValues:(id)keyValues {
	    if (!keyValues) return nil;
	    return [[[self alloc] init] setKeyValues:keyValues];
	}
	- (instancetype)setKeyValues:(id)keyValues {
	    NSArray *propertiesArray = [self.class properties];
	    for (ScottProperty *property in propertiesArray) {
	        ScottPropertyType *type = property.type;
	        Class typeClass = type.typeClass;
	        if (type.isBoolType) {
	            NSLog(@"Bool");
	        }else if (type.isIdType){
	            NSLog(@"ID");
	        }else if (type.isNumberType){
	            NSLog(@"Number");
	        }else{
	            NSLog(@"%@",typeClass);
	        }
	    }
	    return self;
	}

我们想要字典转模型的时候,直接如下使用:

	NSDictionary *dict = @{@"name":@"Scott",
                           @"icon" : @"lufy.png",
                           @"age" : @"20",
                           @"height" : @1.75,
                           @"money" : @"100.9",
                           @"sex" : @(SexMale),
                           @"gay" : @"ture",
                           };
    ScottUser *userModel = [ScottUser objectWithKeyValues:dict];

ok,运行程序,可以看到控制台输出ScottUser类中各属性对应的类型:
图6

我们进行下一步:用该属性名作为键去字典中寻找对应的值

伪代码:

[字典 valueForKey:属性名];

此处的属性名会有点问题,例如我们定义属性名的时候不能是关键字,而如果字典中的key是涉及到关键字的,那么我们需要转换,但是也并非所有的都有这种情况,因此我们可以想到使用代理。我们在NSObject+ScottKeyValue分类中写一个ScottKeyValue协议,并且让它遵守该协议:

	@protocol ScottKeyValue <NSObject>
	@optional
	+ (NSDictionary *)replacedKeyFromPropertyName;
	@end

然后我们提供一个类方法,用于处理将属性名与字典中的key达到一致。

	+ (NSString *)propertyKey:(NSString *)propertyName {
	    NSString *key;
	    if ([self respondsToSelector:@selector(replacedKeyFromPropertyName)]) {
	        key = [self replacedKeyFromPropertyName][propertyName];
	    }
	    return key ? key : propertyName;
	}

调用:

    // 属性名作为键去寻找对应的值
    id value = [keyValues valueForKey:[self.class propertyKey:property.name]];
    if (!value) continue;
	NSLog(@"%@",value);

运行,我们可以看到已经能够拿到值了:
图7

接下来,我们拿到值后将值的类型转换为属性对应的数据类型。

首先需要处理数字类型,如果模型的属性是数字类型,即type.isNumberType == YES,如果字典中的值是字符串类型,需要将其转成NSNumber类型,如果本来就是基本数据类型,则不用进行任何转换。

	if (type.isNumberType == YES) {
       // 字符串-->数字
       if ([value isKindOfClass:[NSString class]]) {
           value = [[[NSNumberFormatter alloc] init] numberFromString:value];
       }
	}

其中有一种情况,是需要进行特殊处理的,当模型的属性是char类型或者bool类型时,获取到的编码都是c,并且bool还有可能是B编码,它们都对应_boolType,因为数字类型包含布尔类型,所以bool类型要在数字类型的条件下进行额外判断。

	if (type.isNumberType == YES) {
        NSString *oldValue = value;
        // 字符串-->数字
        if ([value isKindOfClass:[NSString class]]) {
            value = [[[NSNumberFormatter alloc] init] numberFromString:value];
            if (type.isBoolType) {
                NSString *lower = [oldValue lowercaseString];
                if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"ture"]) {
                    value = @YES;
                }else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]){
                    value = @NO;
                }
            }
		}
	} else { //  然后处理其他类型转化成字符串类型的情况:
          if (typeClass == [NSString class]) {
              if ([value isKindOfClass:[NSNumber class]]) {
                  if (type.isNumberType)
                      // NSNumber -> NSString
                      value = [value description];
              }else if ([value isKindOfClass:[NSURL class]]){
                  // NSURL -> NSString
                  value = [value absoluteString];
              }
          }
      }
	  // 最后赋值      
      [self setValue:value forKey:property.name];

最后我们调用并打印

ScottUser *userModel = [ScottUser objectWithKeyValues:dict];
    NSLog(@"name:%@,icon:%@,age:%d,height:%@,money:%@,sex:%ld,gay:%d",userModel.name,userModel.icon,userModel.age,userModel.height,userModel.money,(long)userModel.sex,userModel.gay);

图8

到这里最简单的字典转模型大致完成了,当然还有很多的细节没有完善,后面再做处理。

JSON字符串转模型

定义一个json字符串转成模型:

	#pragma mark - JSON字符串转模型
	void keyValues2object1(){
	    // 1.定义一个json字符串
	    NSString *jsonString = @"{\"name\":\"scott\",\"icon\":\"lufy.png\",\"age\":20}";
	    // 2.将json字符串转为LZUser模型
	    ScottUser *user = [ScottUser objectWithKeyValues:jsonString];
	    // 3.打印模型属性
	    NSLog(@"name=%@, icon=%@, age=%d",user.name,user.icon,user.age);
	}

运行程序,这时程序会华丽丽的崩溃,因为程序原来只对字典类型作了处理:

	   // 我们可以定位到程序崩溃在这里
	   id value = [keyValues valueForKey:[self.class propertyKey:property.name]];

所以在这之前需要将JSON转成Foundation框架中的对象,苹果提供了强大的NSJSONSerialization,利用它,在刚开始传入字典/JSON字符串的时候将其进行转换。

	- (instancetype)setKeyValues:(id)keyValues { 
	    keyValues = [keyValues JSONObject]; 
	    NSArray *propertiesArray = [self.class properties];
	    ......
	}

该方法的实现如下,如果当前是字符串,则转换成NSData再进行序列化。

	- (id)JSONObject {
	    id foundationObj;
	    if ([self isKindOfClass:[NSString class]]) {
	        NSString *str = (NSString *)self;
	        foundationObj = [NSJSONSerialization  JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
	    }else if ([self isKindOfClass:[NSData class]]){
	        foundationObj = [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
	    }
	    // 如果foundationObj有值,则返回foundationObj,否则返回self
	    return foundationObj ? : self;
	}

此时,运行程序,OK,能够看到控制台能正确输入结果:
图9

复杂字典转模型

定义一个模型中包含模型的复杂字典:

	NSDictionary *dict = @{@"text":@"是啊,今天天气确实不错!",
                           @"user":@{
                                   @"name":@"scott",
                                   @"icon":@"lufy.png"
                                   },
                           @"retweetedStatus":@{
                                   @"text":@"是啊,今天天气确实不错",
                                   @"user":@{
                                           @"name":@"scott_status",
                                           @"icon":@"lufy_status.png"
                                           }
                                   }
                           };

对待这种字典的思路,应该想到递归,当碰到模型中的属性类型是一个模型类时,将字典中的value作为字典处理,然后再调用字典转模型的方法返回一个模型类,所以在包装类型时还要有个属性表示它是否是自定义的模型类,才能作为依据继续递归,判断的方法是看它是否来自于Foundation框架的类。
ScottPropertyType中添加一个属性:

	/**  是否来源于Foundation框架,比如NSString,NSArray等 */
	@property (nonatomic, readonly, getter=isFromFoundation) BOOL fromFoundation;

- (void)getTypeCode:(NSString *)code方法中添加这样一条:

	else if (code.length > 3 && [code hasPrefix:@"@\""]){
        // 去掉@"和",截取中间的类型名称
        code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
        _typeClass = NSClassFromString(code);
        _numberType = (_typeClass == [NSNumber class] || [_typeClass isSubclassOfClass:[NSNumber class]]);
        // 判断是否来自于foundation框架
        _fromFoundation = [NSObject isClassFromFoundation:_typeClass];
    }

NSObject+ScottProperty分类中添加一个类方法:

	// 用于判断当前类是否来自于foundation框架
	+ (BOOL)isClassFromFoundation:(Class)c;

那么问题来了,如果判断是否来自于Foundation框架呢? 下图展示了Foundation框架(NSObject部分)下的类结构:
图10

用一个NSSet(比用NSArray检索效率更高),返回一些常用基本的Foundation框架下继承自NSObject的类。

	static NSSet *foundationClasses_;
	+ (NSSet *)foundationClass {
	    if (foundationClasses_ == nil) {
	        foundationClasses_ = [NSSet setWithObjects:[NSURL class],
	                              [NSDate class],
	                              [NSValue class],
	                              [NSData class],
	                              [NSArray class],
	                              [NSDictionary class],
	                              [NSString class],
	                              [NSMutableString class], nil];
	    }
	    return foundationClasses_;
	}

所以判断是否是foundation框架的类方法具体实现:

	+ (BOOL)isClassFromFoundation:(Class)c {
	    // 因为foundationClasses_里面的类都是继承NSObject,因此NSObject不能放到上面的集合,需要额外做判断
	    if (c == [NSObject class]) return YES;
	    __block BOOL result = NO;
	    [[self foundationClass] enumerateObjectsUsingBlock:^(Class foundationClass, BOOL *stop) {
	        if ([c isSubclassOfClass:foundationClass]) {
	            result = YES;
	            *stop = YES;
	        }
	    }];
	    return result;
	}

得到结果后,需要在NSObject+ScottKeyValue分类中的setKeyValues:方法中添加如下

    // 如果不是来自foundation框架的类并且不是基本数据类型 ,则递归,如果是基本数据类型,typeClass为nil
    if (!type.isFromFoundation && typeClass) {
        value = [typeClass objectWithKeyValues:value];
    }

到这里,复杂字典转模型就算是完成了,具体调用的过程看源码文章结尾会给地址

字典数组转模型

稍微复杂的一种情况是一个字典里面带有数组:

	NSDictionary *dict = @{
                           @"statuses" : @[
                                   @{
                                       @"text" : @"今天天气真不错!",
                                       @"user" : @{
                                               @"name" : @"Rose",
                                               @"icon" : @"nami.png"
                                               }
                                       },
                                   @{
                                       @"text" : @"明天去旅游了",
                                       @"user" : @{
                                               @"name" : @"Jack",
                                               @"icon" : @"lufy.png"
                                               }
                                       }
                                   ],
                           @"ads" : @[
                                   @{
                                       @"image" : @"ad01.png",
                                       @"url" : @"http://www.baidu.com"
                                       },
                                   @{
                                       @"image" : @"ad02.png",
                                       @"url" : @"http://www.sina.com"
                                       }
                                   ],
                           @"totalNumber" : @"2014",
                           @"previousCursor" : @"13476589",
                           @"nextCursor" : @"13476599"
                           };

上面定义了一个字典,创建一个ScottStatusResult模型,里面有两个数组,另外还有其他3个键:

	#import <Foundation/Foundation.h>
	@interface ScottStatusResult : NSObject
	/** 存放着某一页微博数据(里面都是Status模型) */
	@property (nonatomic, strong) NSMutableArray *statuses;
	/** 存放着一堆的广告数据(里面都是Ad模型) */
	@property (nonatomic, strong) NSArray *ads;
	/** 总数 */
	@property (nonatomic, strong) NSNumber *totalNumber;
	/** 上一页的游标 */
	@property (nonatomic, assign) long long previousCursor;
	/** 下一页的游标 */
	@property (nonatomic, assign) long long nextCursor;
	@end

对于一个数组来说,你必须要告诉方法里面装的是什么模型,才能将字典中值为数组的成员转成模型。
MJExtension中,提供了两种方式进行处理。

  • 方式一:调用NSObject分类中的类方法:

    	[ScottStatusResult setupObjectClassInArray:^NSDictionary *{
    	 return @{ @"statuses" : @"ScottStatus",
    	  // 或者 @"statuses" : [ScottStatus class],
    	           @"ads" : @"ScottAd"
    	  // 或者 @"ads" : [ScottAd class]
                 };
    	    }];
    
  • 方式二:在模型的.m文件中实现方法供回调:

    	+ (NSDictionary *)objectClassInArray
    	{
    	    return @{
    	             @"statuses" : @"ScottStatus",
    	              // 或者 @"statuses" : [ScottStatus class],
    	             @"ads" : @"ScottAd"
    	             // 或者 @"ads" : [ScottAd class]
    	             };
    	}
    

原理上都差不多,都是通过代码进行回调,这个主要实现方式二。
NSObject+ScottKeyValue分类中的ScottKeyValue协议中添加一个方法

	+ (NSDictionary *)objectClassInArray;

NSObject+ScottKeyValue分类中的setKeyValues:方法中添加一种类型判断

	// 如果不是来自foundation框架的类并且不是基本数据类型 ,则递归,如果是基本数据类型,typeClass为nil
       if (!type.isFromFoundation && typeClass) {
           value = [typeClass objectWithKeyValues:value];
       }else if ([self.class respondsToSelector:@selector(objectClassInArray)]){ // 看该类是否实现了objectClassInArray方法
           id objectClass;
           objectClass = [self.class objectClassInArray][property.name];
          // 如果是NSString类型
          if ([objectClass isKindOfClass:[NSString class]]) {
              objectClass = NSClassFromString(objectClass);
          }
          if (objectClass) {
              // 返回一个装了模型的数组
              value = [objectClass objectArrayWithKeyValuesArray:value];
          }
      }

返回一个装了模型的数组方法实现:

	/**
	 *  根据字典/JSON返回模型数组
	 *
	 *  @param keyValuesArray 字典/JSON数组
	 *
	 *  @return 模型数组
	 */
	+ (NSMutableArray *)objectArrayWithKeyValuesArray:(id)keyValuesArray
	{
	    if ([self isClassFromFoundation:self])
	        return keyValuesArray;
	    keyValuesArray = [keyValuesArray JSONObject];
	    NSMutableArray *modelArray = [NSMutableArray array];
	    // 遍历
	    for (NSDictionary *keyValues in keyValuesArray) {
	        id model;
	        model = [self objectWithKeyValues:keyValues];
	        if (model) {
	            [modelArray addObject:model];
	        }
	    }    
	    return modelArray;
	}

到这里,字典数组转模型就算是完成了,具体调用的过程看源码文章结尾会给地址

key的替换

在实际开发中,服务器通常返回一个字段名id,或者descriptionJSON数据,而这两个名字在OC中有特殊含义,在定义属性的时候并不能使用这类名称.这时属性名与字典key不再是直接对应的关系,需要加入一层转换。
这个在前面用该属性名作为键去字典中寻找对应的值讲到过,在次就不再重复讲解。

性能优化

将5个字典转模型的例子同时运行,在NSObject+ScottProperty分类中的+ (NSArray *)properties方法中添加一句打印NSLog(@"%@调用了properties方法",[self class]);。另外,之前的例子都是有内存泄露的,这里添加了free(properties);修复了这个问题。

	+ (NSArray *)properties {
	    NSLog(@"%@调用了properties方法",[self class]);
	    NSMutableArray *propertiesArr = [NSMutableArray array];
	    unsigned int outCount = 0;
	    objc_property_t *properties = class_copyPropertyList(self, &outCount);
	    for (int i=0; i<outCount; i++) {
	        objc_property_t property = properties[i];
	        ScottProperty *propertyObj = [ScottProperty propertyWithProperty:property];
	        [propertiesArr addObject:propertyObj];
	    }
	    free(properties);
	    return propertiesArr;
	}

运行程序,可以看到控制台输出:
图11
可以看到,很多的类都不止一次调用了获取属性的方法,对于一个类来说,要获取它的全部属性,只要获取一次就够了.获取到后将结果缓存起来,下次就不必进行不必要的计算。
下面进行优化:

	// 设置一个全局字典用来将类的属性都缓存起来
	static NSMutableDictionary *cachedProperties_;
	+ (void)load
	{
	    cachedProperties_ = [NSMutableDictionary dictionary];
	}

将方法改写为:

	+ (NSArray *)properties {
	    NSMutableArray *propertiesArr = cachedProperties_[NSStringFromClass(self)];
	    if (!propertiesArr) {
	        NSLog(@"%@调用了properties方法",[self class]);
	        propertiesArr = [NSMutableArray array];
	        unsigned int outCount = 0;
	        objc_property_t *properties = class_copyPropertyList(self, &outCount);
	        for (int i=0; i<outCount; i++) {
	            objc_property_t property = properties[i];
	            ScottProperty *propertyObj = [ScottProperty propertyWithProperty:property];
	            [propertiesArr addObject:propertyObj];
	            //        NSLog(@"name:%@--type:%@",propertyObj.name,propertyObj.type.typeClass);
	            // 为了以后方便使用,将C字符串转化成OC对象
	            //        char *name = property_getName(property);
	            //        NSString *name = @(property_getName(property));
	            //        char *att = property_getAttributes(property);
	            //        NSString *att = @(property_getAttributes(property));
	            //        NSUInteger loc = 1;
	            //        NSUInteger len = [att rangeOfString:@","].location - loc;
	            //        NSString *type = [att substringWithRange:NSMakeRange(loc, len)];
	            //        NSLog(@"%@",type);
	        }
	        free(properties);
	        cachedProperties_[NSStringFromClass(self)] = propertiesArr;
	    }
	    return propertiesArr;
	}

此时,控制台输出:
图12
可以看出每一个类只经过一次就可以获取所有属性。


除了缓存属性外,提取类型编码的过程也可以进一步缓存优化性能。
在下面的方法中加上一句打印:

	- (void)getTypeCode:(NSString *)code {
	    NSLog(@"%@",code);
	    ...
	}

可以看到控制台输出:
图13

可以看到一些常用的类型例如NSString多次调用了该方法。提取类型时,只要知道类名(在这里也就是typeCode),一个ScottPropertyType就已经可以确定了。

重写了- initWithTypeString:方法:

	static NSMutableDictionary *cacheTypes_;
	+ (void)load {
	    cacheTypes_ = [NSMutableDictionary dictionary];
	}
	+ (instancetype)propertiesWithAttributeString:(NSString *)att {
	    return [[ScottPropertyType alloc] initWithTypeString:att];
	}
	- (instancetype)initWithTypeString:(NSString *)typeString {   
	    if (self = [super init]) {
	        NSUInteger loc = 1;
	        NSUInteger len = [typeString rangeOfString:@","].location - loc;
	        NSString *typeCode = [typeString substringWithRange:NSMakeRange(loc, len)];
	        if (!cacheTypes_[typeCode]) {
	            [self getTypeCode:typeCode];
	            cacheTypes_[typeCode] = self;
	        }
	    }
	    return self;
	}

输出结果:
图14

结束语

OK,到这里,我们的解读也算是完成了,由于是下班之后写的,所以花费了4天的时间,终于把此篇文章写完了,欢迎大家点评并讨论。
最后代码地址:--->戳这里

参考资料

参考资料:跟着MJExtension实现简单的字典转模型

posted @ 2018-04-25 22:07  Scott_Mr  阅读(291)  评论(0编辑  收藏  举报