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.hNSObject+ObjectMap.m。从名字可以看出,它应该是实现了NSObject类的一个category,的确也是如此的。用下面几张图来说说这个库具体干了些什么。

  1. APP通过API获得Server提供的数据是JSON格式的,如下👇
    JSON
  2. 该JSON格式的数据经过AFNetworking返回给APP的数据形式是NSDictionary({…})或者NSArray([…])
  3. 这些数据经过NSObject+ObjectMap转化成最终的model对象,如下👇
    Model

正经的源码分析

准备工作

从Network Request中得到的数据如下👇
JSON
调用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如下👇
property

遍历传入的NSDictionary:dict,为ProjectTopics类的各个属性赋值

先再来看看这个dict👇
JSON
因为传入dict中的每个key其实就是ProjectTopics类中的每个属性名,所以就可以直接赋值了。当然其中也分3中情况,

  1. 最简单的情况,这个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;
      该函数的实现如下
             return [[NSString stringWithUTF8String:property_getAttributes(property)] componentsSeparatedByString:@","][0];
      
      它返回的NSString是长这样的:T@”NSDate”,&,N,V_last_activity_at / T@”NSString”,&,N,V_company;所以可以根据是否有NSDate进行判断
  2. 如果这个key对应的是一个NSDictionary对象,那么说明这个属性也是另外一个Model对象,对该属性递归调用上面方法即可实现赋值。代码如下👇
           NSString *propertyType = [newObject classOfPropertyNamed:propertyName];
           id nestedObj = [NSObject objectOfClass:propertyType fromJSON:[dict objectForKey:key]];
           [newObject setValue:nestedObj forKey:propertyName];
    
    其中的propertyType其实就是通过上面讲道的property_getAttributes函数,并解析T@"User",&,N,V_owner / T@"Project",&,N,V_project这种类似的字符串,来得到这个Model对象的类型(比如 User, Project类型等)
  3. 如果这个key对应的是一个NSArray对象,那么就得通过ProjectTopics.m中的propertyArrayMap得知这个NSArray里面装的到底是什么对象。
    先来看看ProjectTopics.m里面对propertyArrayMap的设置:
           _propertyArrayMap = [NSDictionary dictionaryWithObjectsAndKeys:@"ProjectTopic", @"list", nil];
    
    从上面可以看出,ProjectTopicslist属性是一个NSArray对象,这个NSArray里面装的是一个一个的ProjectTopic对象。这就好办了,对这些ProjectTopic对象按照上述的方法对他们进行赋值。

总结

总算写完了。。。感觉写的很乱。。。
总之就是对关于对象的几个属性进行操作:

  1. [[NSClassFromString(object) alloc] init];
    这个函数可以根据字符串获得对象,比如根据”ProjectTopics”来初始化一个ProjectTopics对象
  2. objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
    这个函数可以获得对象的所有属性,并返回一个都是由objc_property_t组成的数组
  3. const char *property_getName(objc_property_t property)
    这个函数可以获得属性的属性名,意思就是说objc_property_t是属性,但就是不能以字符串显示出来,这个函数就可以将属性显示出来(比如:”list”啊,”page”啊,”totalRow”啊 等等)
  4. objc_property_t class_getProperty(Class cls, const char *name)
    这个函数和3相反,根据属性名获得属性 (感觉objc_property_t就是一个难以表述的东西)
  5. const char *property_getAttributes(objc_property_t property)
    这个函数根据属性返回属性的类型,比如说owner属性是一个User对象,那么该函数就返回@"User",&,N,V_owner
  6. [newObject setValue:[dict objectForKey:key] forKey:propertyName];
    给属性赋值, 比如propertyName是owner, 那么就给他赋一个User对象。简单粗暴

参考

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器