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个元素,即为包含着属性类型的字符串,返回然后判断即可。

<strong>小结</strong>:至此,将{}类型的 JSON 数据转换为对象就结束了。下边就是[]类型的 JSON 数据转换了。

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

  1. 在JSON数据解析的入口方法中,如果根对象是[]类型,就遍历这个数组,对数组中的每个元素都调用转换单个对象的方法+(id)objectOfClass:(Class)objectClass fromJSON:(NSDictionary *)dict
  2. 如果某个字段对应的数据又是[]类型,则需要调用数组转换的方法arrayMapFromArray: forPropertyName:,方法的第一个参数是[]数据,第二个参数是对应的数组属性中元素的类型,例如,假如模型对象中有@property (nonatomic, strong) NSArray *fruitColorsfruitColors中存放的是NSString,那就第二个参数就传入NSString即可。这个方法的内部处理逻辑和objectOfClass: fromJSON:的基本一致,所以不再贴代码了。我也纳闷,为什么作者把同样的处理逻辑写了两次呢?如果有大神知道,希望在评论中告知,谢谢啦。

<strong>最后</strong>:这个分类对外只暴露了一个解析 JSON 的方法:

+ (NSArray *)arrayOfType:(Class)objectClass FromJSONData:(NSData *)data {
    return [NSObject objectOfClass:objectClass fromJSONData:data];
}

直接就把 JSON 解析的入口方法处理的结果进行了返回。
这里有一点我不太明白的是,这个对外的方法返回类型是NSArray,可是假如 JSON 中只有一个字典数据,那转换之后其实就是一个模型对象。我写了一个很简单的 JSON 串试了一下,虽然用的还是 NSArray 对象接收,但这个对象的 class 就是模型类。我想,是不是在上边这个对外的方法中,用数组包一下比较好呢?这样外界拿到的就始终是NSArray了。有不同意见欢迎评论或者私信,咱们共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • 概述 ​ iOS源码解析—YYModel(YYClassInfo)分析了如何根据OC的Class对象构建...
    egoCogito_panf阅读 11,490评论 4 32
  • 导语:YYModel库是优秀的模型转换库,可自动处理模型转换(从JSON到Model 和 Model到JSON)的...
    南华coder阅读 5,418评论 0 11
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • JSON JSON和XML都是需要解析的 JSON是一种轻量级的数据格式,一般用于数据交互服务器返回给客户端的数据...
    JonesCxy阅读 1,838评论 2 10