前言

iOS应用从API获得的数据有很重要的一部分是以JSON格式返回的,通过AFNetworking,这种JSON格式会被转换成NSDictionary或者NSArray,从而被APP所接收。
这时,如果能将这些NSDictionary/NSArray转化成APP中已经封装好的各种Model,将极大的简化APP的开发。
有很多第三方库已经实现了这个功能,比较知名的就有MJExtension

MJExtension: A fast, convenient and nonintrusive conversion between JSON and model. Your model class don’t need to extend another base class. You don’t need to modify any model file.

嗯,这个第三方库很有名,功能强大,使用也方便,但是本文并不介绍它。在这篇文章里面,我将介绍一个小的从JSON到Model进行转换的第三方库,这个库小到没有一个像样的名字,我在github上也只找到了一个模糊的地址:uacaps/NSObject-ObjectMap ( 不能确认是否是本文所介绍的)。
另外,我也将本文所介绍的这个第三方库上传到我的github上去了。

正文

说正事,这个第三方库只包含2个文件,NSObject+ObjectMap.hNSObject+ObjectMap.m。从名字可以看出,它应该是实现了NSObject类的一个category,的确也是如此的。用下面几张图来说说这个库具体干了些什么。

  1. APP通过API获得Server提供的数据是JSON格式的,如下👇
    JSON
  2. 该JSON格式的数据经过AFNetworking返回给APP的数据形式是NSDictionary({…})或者NSArray([…])
  3. 这些数据经过NSObject+ObjectMap转化成最终的model对象,如下👇
    Model

正经的源码分析

准备工作

从Network Request中得到的数据如下👇
JSON
调用NSObject+ObjectMap中的函数,将JSON->Model

1
2
id resultData = [data valueForKeyPath:@"data"];
ProjectTopics *resultT = [NSObject objectOfClass:@"ProjectTopics" fromJSON:resultData];

正经实现

+(id)objectOfClass:(NSString *)object fromJSON:(NSDictionary *)dict;
该函数是本文分析的核心,它实现了将JSON->Model,来看看它具体是如何实现的。

初始化Model对象(例子中使用的是ProjectTopics对象),借助NSClassFromString的帮助

1
id newObject = [[NSClassFromString(object) alloc] init];

获取ProjectTopics对象的全部属性

1
[newObject propertyDictionary];

具体看看如何实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (NSDictionary *)propertyDictionary {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];

unsigned count;
objc_property_t *properties = class_copyPropertyList([self class], &count);

for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
[dict setObject:key forKey:key];
}

free(properties);

// Add all superclass properties as well, until it hits NSObject
NSString *superClassName = [[self superclass] nameOfClass];
if (![superClassName isEqualToString:@"NSObject"]) {
for (NSString *property in [[[self superclass] propertyDictionary] allKeys]) {
[dict setObject:property forKey:property];
}
}

return dict;
}

** 关键函数 **

  • objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount):
    该函数将ProjectTopics类的属性拷贝到一个数组中,并返回该数组。数组中的每个元素是objc_property_t类型
  • const char *property_getName(objc_property_t property)
    该函数传入一个objc_property_t参数,并返回该属性的属性名

** 关键步骤 **

  • 获取ProjectTopics类的所有属性名,将所有属性名添加入一个NSDictionary
    • 如果父类不是NSObject,也将父类的所有属性名加入同一个NSDictionary

最终得到的NSDictionary如下👇
property

遍历传入的NSDictionary:dict,为ProjectTopics类的各个属性赋值

先再来看看这个dict👇
JSON
因为传入dict中的每个key其实就是ProjectTopics类中的每个属性名,所以就可以直接赋值了。当然其中也分3中情况,

  1. 最简单的情况,这个key对应的值既不是NSArray,也不是NSDictionary(比如id, owner_id等)。对于这种情况,直接赋值,代码如下👇
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    objc_property_t property = class_getProperty([newObject class], [propertyName UTF8String]);
    if (!property) {
    continue;
    }
    NSString *classType = [newObject typeFromProperty:property];

    // check if NSDate or not
    if ([classType isEqualToString:@"T@\"NSDate\""]) {
    // 1970年的long型数字
    NSObject *obj = [dict objectForKey:key];
    if ([obj isKindOfClass:[NSNumber class]]) {
    NSNumber *timeSince1970 = (NSNumber *)obj;
    NSTimeInterval timeSince1970TimeInterval = timeSince1970.doubleValue/1000;
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeSince1970TimeInterval];
    [newObject setValue:date forKey:propertyName];
    }else{
    // 日期字符串
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:OMDateFormat];
    [formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:OMTimeZone]];
    NSString *dateString = [[dict objectForKey:key] stringByReplacingOccurrencesOfString:@"T" withString:@" "];
    [newObject setValue:[formatter dateFromString:dateString] forKey:propertyName];
    }
    }
    else {
    if ([dict objectForKey:key] != [NSNull null]) {
    [newObject setValue:[dict objectForKey:key] forKey:propertyName];
    }
    else {
    [newObject setValue:nil forKey:propertyName];
    }
    }
    上述代码说了很多,但是主要是区分该属性是否是一个NSDate,如果不是的话就直接通过下面代码赋值就完成了
    1
    [newObject setValue:[dict objectForKey:key] forKey:propertyName]; // KVO
    至于如何判断属性是否是NSDate,有以下几个关键函数
  • objc_property_t class_getProperty(Class cls, const char *name)
    该函数传入类和字符串,返回一个objc_property_t类型的属性
  • - (NSString *)typeFromProperty:(objc_property_t)property;
    该函数的实现如下
    1
    return [[NSString stringWithUTF8String:property_getAttributes(property)] componentsSeparatedByString:@","][0];
    它返回的NSString是长这样的:T@”NSDate”,&,N,V_last_activity_at / T@”NSString”,&,N,V_company;所以可以根据是否有NSDate进行判断
  1. 如果这个key对应的是一个NSDictionary对象,那么说明这个属性也是另外一个Model对象,对该属性递归调用上面方法即可实现赋值。代码如下👇
    1
    2
    3
    NSString *propertyType = [newObject classOfPropertyNamed:propertyName];
    id nestedObj = [NSObject objectOfClass:propertyType fromJSON:[dict objectForKey:key]];
    [newObject setValue:nestedObj forKey:propertyName];
    其中的propertyType其实就是通过上面讲道的property_getAttributes函数,并解析T@"User",&,N,V_owner / T@"Project",&,N,V_project这种类似的字符串,来得到这个Model对象的类型(比如 User, Project类型等)
  2. 如果这个key对应的是一个NSArray对象,那么就得通过ProjectTopics.m中的propertyArrayMap得知这个NSArray里面装的到底是什么对象。
    先来看看ProjectTopics.m里面对propertyArrayMap的设置:
    1
    _propertyArrayMap = [NSDictionary dictionaryWithObjectsAndKeys:@"ProjectTopic", @"list", nil];
    从上面可以看出,ProjectTopicslist属性是一个NSArray对象,这个NSArray里面装的是一个一个的ProjectTopic对象。这就好办了,对这些ProjectTopic对象按照上述的方法对他们进行赋值。

总结

总算写完了。。。感觉写的很乱。。。
总之就是对关于对象的几个属性进行操作:

  1. [[NSClassFromString(object) alloc] init];
    这个函数可以根据字符串获得对象,比如根据”ProjectTopics”来初始化一个ProjectTopics对象
  2. objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
    这个函数可以获得对象的所有属性,并返回一个都是由objc_property_t组成的数组
  3. const char *property_getName(objc_property_t property)
    这个函数可以获得属性的属性名,意思就是说objc_property_t是属性,但就是不能以字符串显示出来,这个函数就可以将属性显示出来(比如:”list”啊,”page”啊,”totalRow”啊 等等)
  4. objc_property_t class_getProperty(Class cls, const char *name)
    这个函数和3相反,根据属性名获得属性 (感觉objc_property_t就是一个难以表述的东西)
  5. const char *property_getAttributes(objc_property_t property)
    这个函数根据属性返回属性的类型,比如说owner属性是一个User对象,那么该函数就返回@"User",&,N,V_owner
  6. [newObject setValue:[dict objectForKey:key] forKey:propertyName];
    给属性赋值, 比如propertyName是owner, 那么就给他赋一个User对象。简单粗暴

参考