前言
我们都知道, 开发中会有这么一个过程, 就是将服务器返回的数据转换成我们自己定义的模型对象. 当然服务器返回的数据结构有 xml 类型的, 也有 json 类型的. 本文只讨论 json 格式的.
大家在项目中一般是怎么样将服务器返回的 json 转化成自己定义的模型类呢?
我在项目中一般都是使用的 https://github.com/CoderMJLee/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
方法.
我们可以通过写一个框架自动帮我们实现字典转模型, 大致思路就是:
遍历模型中的属性, 然后拿到属性名作为键值去字典中寻找值;
找到值后, 根据模型的属性类型将值转化成正确的类型;
给属性名赋值.
遍历模型中的属性, 拿到属性名作为键值去字典中寻找值.
方法伪代码:
[模型类 遍历属性的方法];
为了方便使用, 创建一个叫
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];
运行程序, 能够看到控制台的输出:
从输出中可以看到通过 property_getName()获取每一个 objc_property_t 的 name 表示成员属性的名字, 通过
property_getAttributes()
获取每一个 objc_property_t 的 attributes 表示成员属性中的一些特性(如是什么类, 原子性还是非原子性, 是 strong 还是 weak 还是 copy, 生成的成员变量名等信息...)
从苹果的官方文档(Objective-C Runtime Programming Guide) https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html 可以得知, 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);
- }
控制台结果显示, 我们能够截取到其中的类型了:
回归到我们拿到这些属性类型的初衷, 是为了用字典中的值的类型与模型中属性的类型进行对比, 想要对比, 需要拿到属性的类型, 因此需要将这些编码转换成一个表示类的类, 创建一个类用来包装类型.
#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 的提取.
运行重构之后的代码, 可以看到和重构之前是一样的:
上面提到的这些类型, 是类型编码, 在苹果文档中告诉我们编码对应的类型:
根据这个对应关系的图表, 我们将常用的几个编码定义成常量字符串或者宏表示它所对应的类型, 利于编码和阅读:
在 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);
- }
从图中可以看出, 属于基本类型的属性打印出来的类型是 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 类中各属性对应的类型:
我们进行下一步: 用该属性名作为键去字典中寻找对应的值
伪代码:
[字典 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);
运行, 我们可以看到已经能够拿到值了:
接下来, 我们拿到值后将值的类型转换为属性对应的数据类型.
首先需要处理数字类型, 如果模型的属性是数字类型, 即
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);
到这里最简单的字典转模型大致完成了, 当然还有很多的细节没有完善, 后面再做处理.
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, 能够看到控制台能正确输入结果:
复杂字典转模型
定义一个模型中包含模型的复杂字典:
- 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 部分)
下的类结构:
用一个 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, 或者 description 的 JSON 数据, 而这两个名字在 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;
- }
运行程序, 可以看到控制台输出:
可以看到, 很多的类都不止一次调用了获取属性的方法, 对于一个类来说, 要获取它的全部属性, 只要获取一次就够了. 获取到后将结果缓存起来, 下次就不必进行不必要的计算.
下面进行优化:
- // 设置一个全局字典用来将类的属性都缓存起来
- 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;
- }
此时, 控制台输出:
可以看出每一个类只经过一次就可以获取所有属性.
除了缓存属性外, 提取类型编码的过程也可以进一步缓存优化性能.
在下面的方法中加上一句打印:
- - (void)getTypeCode:(NSString *)code {
- NSLog(@"%@",code);
- ...
- }
可以看到控制台输出:
可以看到一些常用的类型例如 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;
- }
输出结果:
结束语
OK, 到这里, 我们的解读也算是完成了, 由于是下班之后写的, 所以花费了 4 天的时间, 终于把此篇文章写完了, 欢迎大家点评并讨论.
最后代码地址:--->戳这里 https://github.com/LZAscott/ScottExtension
参考资料
参考资料: 跟着 MJExtension 实现简单的字典转模型 http://www.jianshu.com/p/d2ecef03f19e
来源: https://www.cnblogs.com/scott-mr/p/8947613.html