iOS开发之-NSObject-ObjectMap源码分析

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 方法,获取到该属性的属性字符串。举例来说,假如有一个 NSStringtestString属性,对应的属性字符串为: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 数据转换了。

[]类型的数据的解析,主要是两点:

  1. 在JSON数据解析的入口方法中,如果根对象是[]类型,就遍历这个数组,对数组中的每个元素都调用转换单个对象的方法+(id)objectOfClass:(Class)objectClass fromJSON:(NSDictionary *)dict
  2. 如果某个字段对应的数据又是[]类型,则需要调用数组转换的方法arrayMapFromArray: forPropertyName:,方法的第一个参数是[]数据,第二个参数是对应的数组属性中元素的类型,例如,假如模型对象中有@property (nonatomic, strong) NSArray *fruitColorsfruitColors中存放的是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了。有不同意见欢迎评论或者私信,咱们共同进步。

你可能感兴趣的:(iOS开发之-NSObject-ObjectMap源码分析)