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
id resultData = [data valueForKeyPath:@"data"];
ProjectTopics *resultT = [NSObject objectOfClass:@"ProjectTopics" fromJSON:resultData];
正经实现
+(id)objectOfClass:(NSString *)object fromJSON:(NSDictionary *)dict;
该函数是本文分析的核心,它实现了将JSON->Model,来看看它具体是如何实现的。
初始化Model对象(例子中使用的是ProjectTopics
对象),借助NSClassFromString
的帮助
id newObject = [[NSClassFromString(object) alloc] init];
获取ProjectTopics
对象的全部属性
[newObject propertyDictionary];
具体看看如何实现
- (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如下👇
遍历传入的NSDictionary:dict
,为ProjectTopics类的各个属性赋值
先再来看看这个dict👇
因为传入dict
中的每个key其实就是ProjectTopics类中的每个属性名,所以就可以直接赋值了。当然其中也分3中情况,
最简单的情况,这个key对应的值既不是NSArray,也不是NSDictionary(比如id, owner_id等)。对于这种情况,直接赋值,代码如下👇
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,如果不是的话就直接通过下面代码赋值就完成了
[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;
该函数的实现如下
它返回的NSString是长这样的:T@”NSDate”,&,N,V_last_activity_at / T@”NSString”,&,N,V_company;所以可以根据是否有NSDate进行判断return [[NSString stringWithUTF8String:property_getAttributes(property)] componentsSeparatedByString:@","][0];
- 如果这个key对应的是一个NSDictionary对象,那么说明这个属性也是另外一个Model对象,对该属性递归调用上面方法即可实现赋值。代码如下👇
其中的propertyType其实就是通过上面讲道的NSString *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的设置:
从上面可以看出,_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对象。简单粗暴