NSObject+ObjectMap 源码分析
前言
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.h
和NSObject+ObjectMap.m
。从名字可以看出,它应该是实现了NSObject类的一个category,的确也是如此的。用下面几张图来说说这个库具体干了些什么。
- APP通过API获得Server提供的数据是JSON格式的,如下👇
- 该JSON格式的数据经过AFNetworking返回给APP的数据形式是
NSDictionary
({…})或者NSArray
([…]) - 这些数据经过
NSObject+ObjectMap
转化成最终的model对象,如下👇
正经的源码分析
准备工作
从Network Request中得到的数据如下👇
调用NSObject+ObjectMap中的函数,将JSON->Model
1 | id resultData = [data valueForKeyPath:@"data"]; |
正经实现
+(id)objectOfClass:(NSString *)object fromJSON:(NSDictionary *)dict;
该函数是本文分析的核心,它实现了将JSON->Model,来看看它具体是如何实现的。
初始化Model对象(例子中使用的是ProjectTopics
对象),借助NSClassFromString
的帮助
1 | id newObject = [[NSClassFromString(object) alloc] init]; |
获取ProjectTopics
对象的全部属性
1 | [newObject propertyDictionary]; |
具体看看如何实现
1 | - (NSDictionary *)propertyDictionary { |
** 关键函数 **
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如下👇
遍历传入的NSDictionary:dict
,为ProjectTopics类的各个属性赋值
先再来看看这个dict👇
因为传入dict
中的每个key其实就是ProjectTopics类中的每个属性名,所以就可以直接赋值了。当然其中也分3中情况,
- 最简单的情况,这个key对应的值既不是NSArray,也不是NSDictionary(比如id, owner_id等)。对于这种情况,直接赋值,代码如下👇 上述代码说了很多,但是主要是区分该属性是否是一个NSDate,如果不是的话就直接通过下面代码赋值就完成了
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
32objc_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
objc_property_t class_getProperty(Class cls, const char *name)
该函数传入类和字符串,返回一个objc_property_t
类型的属性- (NSString *)typeFromProperty:(objc_property_t)property;
该函数的实现如下它返回的NSString是长这样的:T@”NSDate”,&,N,V_last_activity_at / T@”NSString”,&,N,V_company;所以可以根据是否有NSDate进行判断1
return [[NSString stringWithUTF8String:property_getAttributes(property)] componentsSeparatedByString:@","][0];
- 如果这个key对应的是一个NSDictionary对象,那么说明这个属性也是另外一个Model对象,对该属性递归调用上面方法即可实现赋值。代码如下👇 其中的propertyType其实就是通过上面讲道的
1
2
3NSString *propertyType = [newObject classOfPropertyNamed:propertyName];
id nestedObj = [NSObject objectOfClass:propertyType fromJSON:[dict objectForKey:key]];
[newObject setValue:nestedObj forKey:propertyName];property_getAttributes
函数,并解析T@"User",&,N,V_owner / T@"Project",&,N,V_project
这种类似的字符串,来得到这个Model对象的类型(比如 User, Project类型等) - 如果这个key对应的是一个NSArray对象,那么就得通过
ProjectTopics.m
中的propertyArrayMap
得知这个NSArray里面装的到底是什么对象。
先来看看ProjectTopics.m里面对propertyArrayMap的设置:从上面可以看出,1
_propertyArrayMap = [NSDictionary dictionaryWithObjectsAndKeys:@"ProjectTopic", @"list", nil];
ProjectTopics
的list
属性是一个NSArray对象,这个NSArray里面装的是一个一个的ProjectTopic
对象。这就好办了,对这些ProjectTopic对象按照上述的方法对他们进行赋值。
总结
总算写完了。。。感觉写的很乱。。。
总之就是对关于对象的几个属性进行操作:
[[NSClassFromString(object) alloc] init];
这个函数可以根据字符串获得对象,比如根据”ProjectTopics”来初始化一个ProjectTopics对象objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
这个函数可以获得对象的所有属性,并返回一个都是由objc_property_t
组成的数组const char *property_getName(objc_property_t property)
这个函数可以获得属性的属性名,意思就是说objc_property_t是属性,但就是不能以字符串显示出来,这个函数就可以将属性显示出来(比如:”list”啊,”page”啊,”totalRow”啊 等等)objc_property_t class_getProperty(Class cls, const char *name)
这个函数和3相反,根据属性名获得属性 (感觉objc_property_t就是一个难以表述的东西)const char *property_getAttributes(objc_property_t property)
这个函数根据属性返回属性的类型,比如说owner
属性是一个User
对象,那么该函数就返回@"User",&,N,V_owner
[newObject setValue:[dict objectForKey:key] forKey:propertyName];
给属性赋值, 比如propertyName是owner, 那么就给他赋一个User对象。简单粗暴