NSObject-ObjectMap 是一个比较小众的字典转模型的框架,github 上只有四百多个star,不过功能挺强大。这里我只分析它的 json 解析部分。话不多说,开撸。
JSON 解析入口
#pragma mark - JSONData to Object
+ (id)objectOfClass:(Class)objectClass fromJSONData:(NSData *)jsonData {
NSError *error;
id newObject = nil;
// 1. 先将二进制数据进行反序列化
id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:&error];
// 2. 如果反序列化之后,得到的根对象是 字典 类型
if([jsonObject isKindOfClass:[NSDictionary class]]) {
// 调用 NSDictionary -> Object 的转换方法
newObject = [NSObject objectOfClass:objectClass fromJSON:jsonObject];
}
// 3. 反序列化之后,得到的根对象是 数组 类型
else if([jsonObject isKindOfClass:[NSArray class]]){
// 遍历数组,循环调用 NSDictionary -> Object 的转换方法
NSInteger length = [((NSArray*) jsonObject) count];
NSMutableArray *resultArray = [NSMutableArray arrayWithCapacity:length];
for(NSInteger i = 0; i < length; i++){
[resultArray addObject:[NSObject objectOfClass:objectClass fromJSON:[(NSArray*)jsonObject objectAtIndex:i]]];
}
newObject = [[NSArray alloc] initWithArray:resultArray];
}
// 4. 其他类型,直接返回
return newObject;
}
这是 JSON 转换的最顶端的入口函数,逻辑比较简单。因为 JSON 数据的最外层,要么是{}
包裹的键值对,要么是[]
包裹的多组{}
,而且开发中基本都是前者居多,前者对应 OC 中的 NSDictionary
或者 对象,后者对应 OC 中的 NSArray
。其中重点就是objectOfClass: fromJSON:
这个解析{}
的方法了。
JSON 解析单个对象
#pragma mark - Dictionary to Object
+(id)objectOfClass:(Class)objectClass fromJSON:(NSDictionary *)dict {
// 1. 如果模型对象正好是字典类型,则直接将字典返回。用字典接收字典数据,不用转换,直接返回即可。
if([NSStringFromClass(objectClass) isEqualToString:@"NSDictionary"]){
return dict;
}
id newObject = [[objectClass alloc] init];
// 2. 获取模型对象的所有属性,将其存入一个字典中,而且字典中的键和值都是该对象的属性名称。具体的解释见下边。
// 假如,对象有个 name 属性,那这个方法返回{ @"name" : @"name" }
NSDictionary *mapDictionary = [newObject propertyDictionary];
// 3. 数据是字典类型,所以遍历数据中的所有键值对
for (NSString *key in [dict allKeys]) {
// 3.1 根据数据中的键,取出对应的模型对象中的属性名称,
// 因为通常模型对象中的属性名称是根据数据中的键定义的,它们俩一致;
// 为什么不直接把 key 当作属性名称呢?
// 因为不是所有的数据中的键在模型对象中的属性都有对应,用几个定义几个即可
NSString *propertyName = [mapDictionary objectForKey:key];
// 3.2 如果 JSON 数据中的该字段,模型对象中没有对应的属性,直接跳过
if (!propertyName) {
continue;
}
// 3.3 如果 JSON 数据中的该字段的值是空,那就将属性值置为nil
// JSON 数据的空(null) 和 nil 是不一样的
if ([dict objectForKey:key] == [NSNull null]) {
[newObject setValue:nil forKey:propertyName];
continue;
}
// 3.4 该字段对应的又是一个{}字典,类似{"position" : {"x":"0", "y":"0"}},
// 直接递归调用本方法即可
if ([[dict objectForKey:key] isKindOfClass:[NSDictionary class]]) {
// 3.4.1 获取模型对象该嵌套属性的类型
NSString *propertyType = [newObject classOfPropertyNamed:propertyName];
// 3.4.2 递归调用本方法,将{}转换为对象即可
id nestedObj = [NSObject objectOfClass:NSClassFromString(propertyType) fromJSON:[dict objectForKey:key]];
// 3.4.3 设置模型对象该属性的值
[newObject setValue:nestedObj forKey:propertyName];
}
// 3.5 该字段对应的是一个[]数组,类似{"fruitColors":[{"apple":"red"},{"orange":"yellow"}]},
// 直接递归调用本方法即可
else if ([[dict objectForKey:key] isKindOfClass:[NSArray class]]) {
NSArray *nestedArray = [dict objectForKey:key];
// 3.5.1 无法得知数组中的对象的类型,所以这一点需要用户在模型类中手动指定,具体可见其 ReadMe。
// [self setValue:@"NSString" forKeyPath:@"propertyArrayMap.fruitColors"];
NSString *propertyType = [newObject valueForKeyPath:[NSString stringWithFormat:@"propertyArrayMap.%@", key]];
// 3.5.2 确定了数组中的元素类型之后,就可以直接调用数组类型数据的转换方法
[newObject setValue:[NSObject arrayMapFromArray:nestedArray forPropertyName:propertyType] forKey:propertyName];
}
// 3.6 该字段对应的是字符串或者数字,也就是正常的值
else {
// 3.6.1 先获取模型对象中的该属性
objc_property_t property = class_getProperty([newObject class], [propertyName UTF8String]);
if (property) {
// 3.6.2 根据属性名获取该属性的类型
NSString *classType = [newObject typeFromProperty:property];
// 3.6.3 如果该属性是 NSDate 类型,就将日期字符串转换为 NSDate 对象
// 个人感觉这有点 通过属性的类型,倒推数据中的结构,然后特殊处理的意思
// 如果还需要特殊处理其他的类型,放在这里正合适
if ([classType isEqualToString:@"T@\"NSDate\""]) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:OMDateFormat];
[formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:OMTimeZone]];
[newObject setValue:[formatter dateFromString:[dict objectForKey:key]] forKey:propertyName];
}
else {
[newObject setValue:[dict objectForKey:key] forKey:propertyName];
}
}
}
}
return newObject;
}
这个方法算是 JSON 数据解析的核心了,所以看起来复杂一点。其实它就是在解析数据中的单个键值对数据,只不过该键的值有可能又是{}
字典数据,也有可能是[]
数组数据,也有可能是正常的""
字符串或者数字,当然也有可能是空,所以分情况处理。知道了方法的作用,再理解就很容易了。
获取模型对象的所有属性 propertyDictionary
-(NSDictionary *)propertyDictionary {
// 1. 先添加自己的所有属性
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
unsigned count;
// 1.1 获取本类的属性列表
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (NSInteger i = 0; i < count; i++) {
// 1.2 遍历属性列表,将 objc_property_t 的属性转换为 NSString 的属性名称并保存在字典中
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
[dict setObject:key forKey:key];
}
// 1.3 因为是C语言的接口,所以别忘了释放的操作
free(properties);
// 2 获取本类的所有父类(除了 NSObject)中的属性,因为有可能模型是继承自一个基础的模型类
NSString *superClassName = [[self superclass] nameOfClass];
if (![superClassName isEqualToString:@"NSObject"]) {
// 2.1 遍历的时候,同样也用到了递归
for (NSString *property in [[[self superclass] propertyDictionary] allKeys]) {
[dict setObject:property forKey:property];
}
}
// 3. 返回包含着包括父类的所有属性的字典集合
return dict;
}
这个方法没什么复杂的逻辑,就是通过class_copyPropertyList
方法获取到类的属性列表,然后遍历本身和除了NSObject
的父类,并将属性名称转换为 NSString
类型,然后存入到一个字典中并返回。
根据属性名获取属性的类型
-(NSString *)classOfPropertyNamed:(NSString *)propName {
objc_property_t theProperty = class_getProperty([self class], [propName UTF8String]);
const char *attributes = property_getAttributes(theProperty);
char buffer[1 + strlen(attributes)];
strcpy(buffer, attributes);
char *state = buffer, *attribute;
while ((attribute = strsep(&state, ",")) != NULL) {
if (attribute[0] == 'T' && attribute[1] != '@') {
// it's a C primitive type:
/*
if you want a list of what will be returned for these primitives, search online for
"objective-c" "Property Attribute Description Examples"
apple docs list plenty of examples of what you get for int "i", long "l", unsigned "I", struct, etc.*/
NSString *typeName = [[NSString alloc] initWithData:[NSData dataWithBytes:(attribute + 1) length:strlen(attribute) - 1] encoding:NSUTF8StringEncoding];
return typeName;
}
else if (attribute[0] == 'T' && attribute[1] == '@' && strlen(attribute) == 2) {
// it's an ObjC id type:
return @"id";
}
else if (attribute[0] == 'T' && attribute[1] == '@') {
// it's another ObjC object type:
NSData *data = [NSData dataWithBytes:(attribute + 3) length:strlen(attribute) - 4];
NSString *className = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return className;
}
}
return @"";
}
在这个获取属性的类型的方法里,首先根据属性名和class_getProperty
获取到objc_property_t
属性,然后调用 property_getAttributes
方法,获取到该属性的属性字符串。举例来说,假如有一个 NSString
的 testString
属性,对应的属性字符串为:T@"NSString",&,N,V_testString
,然后用C语言处理,获取到NSString
即达到目的。在上边那个核心方法里,到最后还有一个typeFromProperty
方法,处理的逻辑也是这样,比这个更简单。
-(NSString *)typeFromProperty:(objc_property_t)property {
return [[NSString stringWithUTF8String:property_getAttributes(property)] componentsSeparatedByString:@","][0];
}
用,
分割,然后取出第0个元素,即为包含着属性类型的字符串,返回然后判断即可。
小结:至此,将{}
类型的 JSON 数据转换为对象就结束了。下边就是[]
类型的 JSON 数据转换了。
[]
类型的数据的解析,主要是两点:
- 在JSON数据解析的入口方法中,如果根对象是
[]
类型,就遍历这个数组,对数组中的每个元素都调用转换单个对象的方法+(id)objectOfClass:(Class)objectClass fromJSON:(NSDictionary *)dict
。 - 如果某个字段对应的数据又是
[]
类型,则需要调用数组转换的方法arrayMapFromArray: forPropertyName:
,方法的第一个参数是[]
数据,第二个参数是对应的数组属性中元素的类型,例如,假如模型对象中有@property (nonatomic, strong) NSArray *fruitColors
,fruitColors
中存放的是NSString
,那就第二个参数就传入NSString
即可。这个方法的内部处理逻辑和objectOfClass: fromJSON:
的基本一致,所以不再贴代码了。我也纳闷,为什么作者把同样的处理逻辑写了两次呢?如果有大神知道,希望在评论中告知,谢谢啦。
最后:这个分类对外只暴露了一个解析 JSON 的方法:
+ (NSArray *)arrayOfType:(Class)objectClass FromJSONData:(NSData *)data {
return [NSObject objectOfClass:objectClass fromJSONData:data];
}
直接就把 JSON 解析的入口方法处理的结果进行了返回。
这里有一点我不太明白的是,这个对外的方法返回类型是NSArray
,可是假如 JSON 中只有一个字典数据,那转换之后其实就是一个模型对象。我写了一个很简单的 JSON 串试了一下,虽然用的还是 NSArray
对象接收,但这个对象的 class
就是模型类。我想,是不是在上边这个对外的方法中,用数组包一下比较好呢?这样外界拿到的就始终是NSArray
了。有不同意见欢迎评论或者私信,咱们共同进步。