iOS源碼解析—YYModel(NSObject+YYModel)

概述

? iOS源碼解析—YYModel(YYClassInfo)分析了如何根據OC的Class對象構建YYClassInfo對象,為接下來的JSON數據和Model轉換作準備,這篇文章開始講解NSObject+YYModel。

NSObject+YYModel.h

? 分析NSObject+YYModel.h文件,包括3個Category和一個protocol,分別是:

NSObject(YYModel)

NSArray(YYModel)

NSDictionary(YYModel)

YYModel <NSObject>

  1. NSObject(YYModel)

    負責擴展NSObject類,提供了Model相關方法,下面是代碼注釋:

    @interface NSObject (YYModel)
    //根據JSON對象創建Model
    + (nullable instancetype)yy_modelWithJSON:(id)json;
    //根據NSDictionary創建Model
    + (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;
    //將JSON對象的各字段映射到Model的各個字段
    - (BOOL)yy_modelSetWithJSON:(id)json;
    //將NSDictionary的各字段映射到Model的各個字段
    - (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic;
    //根據Model創建JSON對象
    - (nullable id)yy_modelToJSONObject;
    //根據Model創建JSON數據流
    - (nullable NSData *)yy_modelToJSONData;
    //根據Model創建JSON字符串
    - (nullable NSString *)yy_modelToJSONString;
    //復制Model
    - (nullable id)yy_modelCopy;
    //對Model進行歸檔
    - (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder;
    //對Model進行解檔
    - (id)yy_modelInitWithCoder:(NSCoder *)aDecoder;
    //model的hash值
    - (NSUInteger)yy_modelHash;
    //判斷Model和參數model是否相同
    - (BOOL)yy_modelIsEqual:(id)model;
    //輸出Model的相關信息
    - (NSString *)yy_modelDescription;
    @end
    

    例如定義一個Student類,代碼如下:

    @interface College : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
           
    //Student
    @interface Student : NSObject <YYModel>
    @property (nonatomic, copy) NSString *name; //名字
    @property (nonatomic, assign) NSInteger age; //年齡
    @property (nonatomic, strong) College *college; //學校
    @end
    

    同時創建一個字典對象,如下:

    NSDictionary *studentDic = @{@"name" : @"Tomy",
                                 @"age" : @18,
                                 @"college" : @{@"name" : @"NJU"}};
    

    調用[Student yy_modelWithDictionary:studentDic]方法,創建一個Student類型的Model對象,然后將studentDic的每個value賦值給Model相應屬性。property的名稱和key一一對應。在轉化的過程中,可以實現Model嵌套的情況,如Student中嵌套一個College對象。

    調用[Student yy_modelToJSONObject]方法,創建一個字典對象,各鍵值對中的key是Model相應屬性的名稱,value是屬性值。在轉化的過程中,也支持嵌套的情況。

  2. NSArray(YYModel)

    負責擴展NSArray類,下面是代碼注釋:

     @interface NSArray (YYModel)
     //根據JSON創建Model數組(JSON是數組格式)
     + (nullable NSArray *)yy_modelArrayWithClass:(Class)cls json:(id)json;
     @end
    

    例如有一個數組,如下:

    NSArray *studentArr = 
             @[@{@"name" : @"Tomy", @"age" : @18, @"college" : @{@"name" : @"NJU"}}, 
         @{@"name" : @"Alex", @"age" : @19, @"college" : @{@"name" : @"Harvard"}},
         @{@"name" : @"Sunny", @"age" : @20, @"college" : @{@"name" : @"Yale"}}];
    

    調用[NSArray yy_modelArrayWithClass:Student json:studentArr]方法,創建一個Model數組,數組中的每個元素是Student對象。

  3. NSDictionary(YYModel)

    負責擴展NSDictionary類,下面是代碼注釋:

    @interface NSDictionary (YYModel)
    //根據JSON的value和Class對象創建Model,并創建一個新字典,取json的key作為key,取Model作為value
    + (nullable NSDictionary *)yy_modelDictionaryWithClass:(Class)cls json:(id)json;
    @end
    

    例如創建一個字典,如下:

    NSDictionary *playerDic = 
      @{@"player1" : @{@"name" : @"Tomy",
                       @"age" : @18,
                       @"college" : @{@"name" : @"NJU"}},
      @"player2" : @{@"name" : @"Alex",
                     @"age" : @19,
                     @"college" : @{@"name" : @"Yale"}}};
    

    調用[NSDictionary yy_modelDictionaryWithClass:Student json:playerDic]得到一個新的字典studentDic,如下:

1-6.png

字典studentDic中的key對應原字典playerDic中的key,studentDic中的Model由playerDic中的value轉化得到。

  1. @protocol YYModel

    YYModel是iOS協議,里面聲明了一些方法,調用類通過實現這些方法,實現JSON和Model之間轉換過程中的特殊處理。

    + (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper;
    + (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
    + (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;
    + (nullable NSArray<NSString *> *)modelPropertyBlacklist;
    + (nullable NSArray<NSString *> *)modelPropertyWhitelist;
    - (NSDictionary *)modelCustomWillTransformFromDictionary:(NSDictionary *)dic;
    - (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic;
    - (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic;
    

    下面講一下協議中的每個方法:

    1. modelCustomPropertyMapper方法,可以指定json和model轉化過程中key的映射關系,如果存在以下的字典:

      NSDictionary *studentDic = @{@"NAME" : @"Tomy",
                                     @"AGE" : @{@"num":@18},
                                   @"college" : @{@"name" : @"NJU"}};
      

      如果需要將studentDic轉化成Student類型的model,需要實現如下modelCustomPropertyMapper方法:

      + (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper
      {
        return @{@"name" : @"NAME",
                       @"age" : @"AGE.num"};
      }
      

      返回的鍵值對中的key是Student的property名稱,value是studentDic中的key。

    2. 如果Student中存在property是對象數組或者字典,實現modelContainerPropertyGenericClass方法,例如Student存在屬性mobilePhones,維護一組MobilePhone對象

      @interface MobilePhone : NSObject
      @property (nonatomic, copy) NSString *brand;
      @property (nonatomic, assign) NSInteger phoneNumber;
      @end
        
      @interface Student : NSObject <YYModel>
      @property (nonatomic, copy) NSString *name;
      @property (nonatomic, assign) NSInteger age;
      @property (nonatomic, strong) College *college;
      @property (nonatomic, strong) NSArray *mobilePhones; //MobilePhone對象數組
      @end
      NSDictionary *studentDic = 
        @{@"name" : @"Tomy",
          @"age" : @18,
          @"college" : @{@"name" : @"NJU"},
          @"mobilePhones" : @[@{@"brand" : @"iphone",@"phoneNumber" : @123456},
                              @{@"brand" : @"HUAWEI",@"phoneNumber" : @123456}]};
      

      調用[Student yy_modelWithDictionary:studentDic]方法將studentDic中的mobilePhones轉化成Student的mobilePhones屬性,需要實現如下:

      + (nullable NSDictionary<NSString*, id>*)modelContainerPropertyGenericClass
      {
          return @{@"mobilePhones" : [MobilePhone class]};
      }
      
    3. modelCustomClassForDictionary方法用于指定生成Model的類型,如果沒有實現該方法,用默認的類型,例如Student實現modelCustomClassForDictionary,如下:

      + (Class)modelCustomClassForDictionary:(NSDictionary*)dictionary {
        if ([dictionary[@"name"] isEqualToString @"Graduated"]) {
            return [Graduated class];
            } else {
            return [self class];
            }
      }
      

      調用[Student yy_modelWithDictionary:studentDic]方法,如果studentDic[@"name"]等于@"Graduated",則不創建Student類型的對象,而是創建Graduated類型的對象。

    4. modelPropertyBlacklist方法和modelPropertyWhitelist方法的作用相反,如果實現了modelPropertyBlacklist,該方法返回一個數組,數組中的值指定了Model中不需要被賦值的property,例如代碼如下:

      + (nullable NSArray<NSString *> *)modelPropertyBlacklist
      {
          return @[@"age", @"college"];
      }
      

      則Student中的age和college屬性不會被賦值。如果實現了modelPropertyWhitelist方法,該方法返回一個數組,數組中的值指定了Model中僅需要被賦值的property,例如代碼如下:

      + (nullable NSArray<NSString *> *)modelPropertyWhitelist
      {
          return @[@"age", @"college"];
      }
      

      則Student中只有age和college屬性會被賦值。

    5. modelCustomWillTransformFromDictionary:方法作用于根據JSON創建Model對象之前,該方法可以把JSON字典轉化成一個新的字典。

    6. modelCustomTransformFromDictionary:方法作用于根據JSON創建Model對象之后,該方法可以對生成的Model做一些處理。

    7. modelCustomTransformToDictionary:方法作用于根據Model對象創建JSON之后,該方法可以對生成的JSON字典做一些處理。

NSObject+YYModel.m

首先定義了兩個類,_YYModelMeta和_YYModelPropertyMeta,分別封裝了Model的信息和Model中各屬性的信息。

_YYModelMeta維護了Class的相關信息,下面是注釋:

@interface _YYModelMeta : NSObject {
    @package
    YYClassInfo *_classInfo; //關聯的YYClassInfo對象
    NSDictionary *_mapper; //維護一個鍵值對,key是屬性名,value是_YYModelPropertyMeta對象
    //維護一個數組,里面的元素是_YYModelPropertyMeta對象
    NSArray *_allPropertyMetas;
    //維護一個數組,里面的元素是_YYModelPropertyMeta對象
    NSArray *_keyPathPropertyMetas;
    //維護一個數組,里面的元素是_YYModelPropertyMeta對象
    NSArray *_multiKeysPropertyMetas;
    //通過映射相關
    NSUInteger _keyMappedCount;
    //對象類型
    YYEncodingNSType _nsType;
    ...
}
@end

_YYModelMeta是通過YYClassInfo對象的信息構建得到的。首先調用metaWithClass:cls方法,該方法如下:

+ (instancetype)metaWithClass:(Class)cls {
    ...
    _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls)); //從緩存中取
    ...
    if (!meta || meta->_classInfo.needUpdate) {
        meta = [[_YYModelMeta alloc] initWithClass:cls]; //創建一個新的_YYModelMeta對象
        if (meta) {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta));
            dispatch_semaphore_signal(lock);
        }
    }
    return meta;
}

維護了一個鍵值對cache作為緩存,用cls作為key,調用CFDictionaryGetValue方法去緩存中查找,如果有,直接返回構建好的_____YYModelMeta對象,如果沒有找到或者needUpdate屬性標記為true,則根據cls創建一個新的_YYModelMeta對象,并且存入緩存。這樣不需要每次都創建,提高了性能。

接下來看一下initWithClass:方法,

  1. 創建一個YYClassInfo對象,在上一篇文章中分析了YYClassInfo的創建方法;

  2. 判斷調用層是否實現modelPropertyBlacklist方法,如果實現了,返回一組黑名單,數組中包含的屬性名不會被轉化。

  3. 判斷調用層是否實現modelPropertyWhitelist方法,如果實現了,返回一組白名單,不在數組中的屬性名不會被轉化。

  4. 如果調用層實現了modelContainerPropertyGenericClass方法,則維護一個字典genericMapper,字典的value如果是NSString類型,需要轉成Class類型。

  5. 遍歷對象的屬性,生成_YYModelPropertyMeta,代碼注釋如下:

    NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
    YYClassInfo *curClassInfo = classInfo;
    while (curClassInfo && curClassInfo.superCls != nil) {
     for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) 
        {
             if (!propertyInfo.name) continue;
             //黑名單過濾
             if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
             //白名單過濾
            if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
             //根據classInfo和propertyInfo創建_YYModelPropertyMeta
            _YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo                                                                  propertyInfo:propertyInfo                                                                       generic:genericMapper[propertyInfo.name]];
             if (!meta || !meta->_name) continue;
            if (!meta->_getter || !meta->_setter) continue;
            if (allPropertyMetas[meta->_name]) continue;
            allPropertyMetas[meta->_name] = meta; //存入allPropertyMetas字典中
        }
        curClassInfo = curClassInfo.superClassInfo;
    }
    if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy; //賦值給_allPropertyMetas屬性
    

    根據classInfo信息和propertyInfo信息創建_YYModelPropertyMeta對象,并用_allPropertyMetas字典存儲,字典中的key是屬性名,value是_YYModelPropertyMeta對象。

  6. 如果調用層實現了modelCustomPropertyMapper方法,說明存在JSON字典key和屬性名之間的映射關系,需要遍歷這些屬性,做特殊處理,如下:

    if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
         NSDictionary *customMapper = [(id <YYModel>)cls modelCustomPropertyMapper];
     [customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *mappedToKey, BOOL *stop) {
             //這些屬性名對應的_YYModelPropertyMeta從allPropertyMetas字典中刪除
             _YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName];
            if (!propertyMeta) return;
            [allPropertyMetas removeObjectForKey:propertyName];
             
             //mappedToKey作為新的key,存入mapper,value是propertyMeta,同時對mappedToKey做一些分割處理,表示映射的路徑
            if ([mappedToKey isKindOfClass:[NSString class]]) {
             ...
                if (keyPath.count > 1) {
                        propertyMeta->_mappedToKeyPath = keyPath;
                        [keyPathPropertyMetas addObject:propertyMeta];
                }
                 propertyMeta->_next = mapper[mappedToKey] ?: nil;
                 mapper[mappedToKey] = propertyMeta;  
             } 
             ...
         }];
    }
    
  7. 維護一個_mapper字典,key是mappedToKey,對于customMapper字典中不存在的屬性,mappedToKey是其本身的屬性名,value是創建的propertyMeta。同時維護一個_keyPathPropertyMetas數組和_multiKeysPropertyMetas數組,專門存儲customMapper中存在的,即存在映射關系的_YYModelPropertyMeta。

    其中YYModelPropertyMeta類維護了property的相關信息,下面是相關注釋:

    @interface _YYModelPropertyMeta : NSObject {
        @package
        NSString *_name;             //屬性名
        YYEncodingType _type;        //屬性類型
        YYEncodingNSType _nsType;    //屬性如果是NSObject類型的對象,相應的類型
        BOOL _isCNumber;             //是否是數字類型
        Class _cls;                  //屬性的類
        Class _genericCls;           //
        SEL _getter;                 //getter方法
        SEL _setter;                 //setter方法
        BOOL _isKVCCompatible;       //是否可以處理KVC
        BOOL _isStructAvailableForKeyedArchiver; //是否可以archiver/unarchiver
        BOOL _hasCustomClassFromDictionary; //
        
        NSString *_mappedToKey;      //該屬性名對應的JSON字典中的key
        NSArray *_mappedToKeyPath;   //該屬性名對應的JSON字典中的keyPath,keyPath是一個路徑,例如AGE.NUM,該數組存儲@[AGE,NUM]
        NSArray *_mappedToKeyArray;  //該屬性名對應的JSON字典中的key,如果@[@"age",@"num"]
        YYClassPropertyInfo *_info;  //關聯的YYClassPropertyInfo
        _YYModelPropertyMeta *_next; //另一個YYModelPropertyMeta對象,key名稱相同
    }
    @end
    

    _YYModelPropertyMeta對象是通過YYClassPropertyInfo對象的信息構建得到的。

主要方法

下面分析幾個重要的方法:

  1. yy_modelWithJSON:方法

    該方法首先調用_yy_dictionaryWithJSON:方法將JSON數據進行轉化,如果JSON數據是NSString或者NSData格式,轉化為字典對象,否則直接返回。然后調用yy_modelWithDictionary:方法,代碼如下:

    + (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
            ...
            Class cls = [self class];
             //1.metaWithClass
            _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
            if (modelMeta->_hasCustomClassFromDictionary) {
                cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
            }
            ...
            //2.根據json字典對新創建的model進行賦值
            NSObject *one = [cls new];
            if ([one yy_modelSetWithDictionary:dictionary]) return one;
            return nil;
        }
    

    該方法主要分為2個步驟:

  • 根據Class對象cls創建_YYModelMeta

  • 調用yy_modelSetWithDictionary:對model進行賦值,主要代碼注釋如下:

    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
               //1、遍歷dic中的key和value,調用ModelSetWithDictionaryFunction方法進行屬性賦值
           CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
          //2、如果_keyPathPropertyMetas存放有映射關系,調用ModelSetWithPropertyMetaArrayFunction方法進行賦值
           if (modelMeta->_keyPathPropertyMetas) {
               CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas, CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                                         ModelSetWithPropertyMetaArrayFunction,
                                         &context);
            }
          //3、如果_multiKeysPropertyMetas存放有映射關系,調用ModelSetWithPropertyMetaArrayFunction方法進行賦值
            if (modelMeta->_multiKeysPropertyMetas) {
                    CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
                                         CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
                                         ModelSetWithPropertyMetaArrayFunction,
                                         &context);
                }
        } else {
            CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
                                     CFRangeMake(0, modelMeta->_keyMappedCount),
                                     ModelSetWithPropertyMetaArrayFunction,
                                     &context);
        }
    

    主要分為2種情況:

    1. 如果Model中不存在屬性名的映射,調用CFDictionaryApplyFunction方法遍歷JSON字典中的每一個鍵值對,調用ModelSetWithDictionaryFunction方法對每一個鍵值對進行處理,由于_YYModelMeta內部維護了一個mapper字典,通過mapper取出YYModelPropertyMeta對象,最終調用ModelSetValueForProperty方法進行該屬性的賦值。方法如下:

      static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
          ModelSetContext *context = _context;
          __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
          //根據key在mapper中查找對應的propertyMeta,如果沒有,不進行后續操作
          __unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)];
          __unsafe_unretained id model = (__bridge id)(context->model);
          //調用方法進行屬性賦值
          while (propertyMeta) {
              if (propertyMeta->_setter) {
                  ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
              }
              propertyMeta = propertyMeta->_next;
          };
      }
      
    2. 如果Model中存在_keyPathPropertyMetas屬性或者_multiKeysPropertyMetas屬性,說明_keyPathPropertyMetas數組或者_multiKeysPropertyMetas數組中維護了一組Model中的屬性,這些屬性的名稱存在映射關系,這種情況下調用CFArrayApplyFunction方法對keyPathPropertyMetas數組或者multiKeysPropertyMetas數組進行遍歷,并調用ModelSetWithPropertyMetaArrayFunction方法對每個屬性元素進行處理,該方法最終也會調用ModelSetValueForProperty方法進行屬性賦值。代碼注釋如下:

      static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) {
          ...
          //根據映射關系找到JSON中的value
          if (propertyMeta->_mappedToKeyArray) {
              value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray);
          } else if (propertyMeta->_mappedToKeyPath) {
              value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath);
          } else {
              value = [dictionary objectForKey:propertyMeta->_mappedToKey];
          }
          //進行屬性賦值
          if (value) {
              __unsafe_unretained id model = (__bridge id)(context->model);
              ModelSetValueForProperty(model, value, propertyMeta);
          }
      }
      

    上述兩種情況最終會調用ModelSetValueForProperty方法在賦值的時候做了類型兼容,下文中具體分析。

  1. yy_modelToJSONObject方法

    該方法調用ModelToJSONObjectRecursive方法將Model對象轉化成一個字典或者數組,需要轉化的Model對象可以分為3類:

    第一類是一些基本類型,直接返回或者做一些處理返回,代碼如下:

    if ([model isKindOfClass:[NSString class]]) return model;
    if ([model isKindOfClass:[NSNumber class]]) return model;
    if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString;
    if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string;
    

    第二類是NSDictionary、NSSet、NSArray等類型,它們不屬于自定義的對象類型,代碼如下:

    if ([model isKindOfClass:[NSDictionary class]]) { //如果是NSDictionary類型,遍歷字典,用遞歸的方式賦值給新的字典,并返回新的字典。
         if ([NSJSONSerialization isValidJSONObject:model]) return model;
             NSMutableDictionary *newDic = [NSMutableDictionary new];
            [((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
                NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description;
                if (!stringKey) return;
                id jsonObj = ModelToJSONObjectRecursive(obj);
                if (!jsonObj) jsonObj = (id)kCFNull;
                newDic[stringKey] = jsonObj;
            }];
            return newDic;
    }
    //如果是NSSet類型,遍歷NSSet,用遞歸的方式賦值給新的set,并返回新的set。
    if ([model isKindOfClass:[NSSet class]]) {
     NSArray *array = ((NSSet *)model).allObjects;
         if ([NSJSONSerialization isValidJSONObject:array]) return array;
     NSMutableArray *newArray = [NSMutableArray new];
     for (id obj in array) {
     if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) {
         [newArray addObject:obj];
     } else {
         id jsonObj = ModelToJSONObjectRecursive(obj);
         if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj];
                }
            }
         return newArray;
    }
    //如果是NSArray類型,遍歷NSArray,用遞歸的方式賦值給新的array,并返回新的array。
    if ([model isKindOfClass:[NSArray class]]) {
            if ([NSJSONSerialization isValidJSONObject:model]) return model;
            NSMutableArray *newArray = [NSMutableArray new];
            for (id obj in (NSArray *)model) {
                if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) {
                    [newArray addObject:obj];
                } else {
                    id jsonObj = ModelToJSONObjectRecursive(obj);
                    if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj];
                }
            }
            return newArray;
    }
         ...
    

    第三種是自定義的Model對象類型,首先調用metaWithClass獲取自定義Model的_YYModelMeta對象,通過上文知道_YYModelMeta內部維護了一個mapper字典,用于維護Model內部的屬性類型信息,然后遍歷mapper中的每一個元素(_YYModelPropertyMeta),根據元素的類型,進行相應的處理,如下:

    [modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
         if (propertyMeta->_isCNumber) { //數值類型
             value = ModelCreateNumberFromProperty(model, propertyMeta);
     } else if (propertyMeta->_nsType) {
             id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
             value = ModelToJSONObjectRecursive(v); //遞歸調用
         } else {
             ...
         }
    }]
    

    如果_YYModelPropertyMeta存在mappedToKeyPath屬性,需要將屬性名映射到指定的key上面。

  2. yy_modelArrayWithClass:json:方法

    該方法通過JSON數組和Class創建Model數組,首先調用yy_modelArrayWithClass: array:方法,然后在該方法中遍歷數組,數組中每個元素是字典類型,調用yy_modelWithDictionary:方法進行轉化,然后將轉化后的Model添加進新數組中,最后返回這個新數組。代碼如下:

    + (NSArray *)yy_modelArrayWithClass:(Class)cls array:(NSArray *)arr {
        if (!cls || !arr) return nil;
        NSMutableArray *result = [NSMutableArray new];
        for (NSDictionary *dic in arr) { //遍歷數組
            if (![dic isKindOfClass:[NSDictionary class]]) continue;
            NSObject *obj = [cls yy_modelWithDictionary:dic]; //dic->Model
            if (obj) [result addObject:obj]; //添加Model
        }
        return result;
    }
    
  3. yy_modelDictionaryWithClass:json:方法

    該方法和上面的方法3類似,首先調用yy_modelDictionaryWithClass:dictionary:方法,然后在該方法中遍歷JSON字典,字典中的每個元素同樣是字典,調用yy_modelWithDictionary:方法進行轉化,然后將轉化后的Model加入新的字典中,key是之前JSON字典中的key。代碼如下:

    + (NSDictionary *)yy_modelDictionaryWithClass:(Class)cls dictionary:(NSDictionary *)dic {
        if (!cls || !dic) return nil;
        NSMutableDictionary *result = [NSMutableDictionary new];
        for (NSString *key in dic.allKeys) { //遍歷字典
            if (![key isKindOfClass:[NSString class]]) continue;
            NSObject *obj = [cls yy_modelWithDictionary:dic[key]]; //dic[key]->Model
            if (obj) result[key] = obj; //添加Model
        }
        return result;
    }
    

類型轉化和兼容

YYModel的總體思想是以Model屬性的類型為準,如果JSON中對應名稱的value的類型和Model屬性類型不一致,會對value的類型進行轉化,保證和Model屬性的類型一致。如果兼容不了,不進行屬性賦值。下面分析一下ModelSetValueForProperty方法:

該方法上文中提到該方法是用來JSON轉成Model的過程中對Model中的屬性進行賦值的方法,該方法做了部分基本類型的兼容:

  • 如果Model的屬性是數值類型,與之對應的JSON的value是其它類型,會進行轉化,如下:

    if (meta->_isCNumber) {
      NSNumber *num = YYNSNumberCreateFromID(value);
      ModelSetNumberToProperty(model, num, meta);
      if (num) [num class]; // hold the number
    }
    

    YYNSNumberCreateFromID會將value轉化成NSNumber類型,有可能返回nil。

  • 如果Model的屬性是字符串類型,會判斷value的類型,進一步處理:

    case YYEncodingTypeNSString:
    case YYEncodingTypeNSMutableString: {
      if ([value isKindOfClass:[NSString class]]) {
          ...
      } else if ([value isKindOfClass:[NSNumber class]]) {
          ... //轉化成NSString
        } else if ([value isKindOfClass:[NSData class]]) {
          ... //轉化成NSString
      } else if ([value isKindOfClass:[NSURL class]]) {
          ... //轉化成NSString
        } else if ([value isKindOfClass:[NSAttributedString class]]) {
          ... //轉化成NSString
        }
    break;
    

    根據value類型的不同,調用不得方法轉成字符串類型。

  • 如果Model的屬性是NSNumber類型,需要將value轉化成NSNumber類型。

  • 如果Model的屬性是NSData、NSDate、NSURL類型等,都需要value先轉成這些類型。

  • 不是所有的類型都可以轉化,如果屬性的對象是數組、集合或者字典類型,但是value不是對應的類型,不會轉化,直接返回,不進行后續的屬性賦值。例如字典類型,代碼如下:

    case YYEncodingTypeNSDictionary:
    case YYEncodingTypeNSMutableDictionary: {
      if ([value isKindOfClass:[NSDictionary class]]) { //value為字典類型是,進行處理,否則不處理
          ... 
        }
    } break;
    

其它方法

YYModel還提供了一些工具方法,下面簡單分析一下:

  1. ModelDescription方法,用來描述該Model對象的相關信息,一般用于log輸出。該方法主要是根據Model對象的類型,做不同的處理:

    如果是一些簡單類型,做一些處理后直接輸出,下面是代碼注釋:

    case YYEncodingTypeNSNumber:
    case YYEncodingTypeNSDecimalNumber:
    case YYEncodingTypeNSDate:
    case YYEncodingTypeNSURL: {
     return [NSString stringWithFormat:@"%@",model];
    }
    

    如果是集合、數組或者字典類型,需要遍歷其中每個元素,遞歸調用ModelDescription,生成每個元素的輸出信息,拼接成一個字符串輸出,例如是NSArray:

    case YYEncodingTypeNSArray: case YYEncodingTypeNSMutableArray: {
     NSArray *array = (id)model;
     NSMutableString *desc = [NSMutableString new];
         if (array.count == 0) {
             return [desc stringByAppendingString:@"[]"];
         } else {
             [desc appendFormat:@"[\n"];
             for (NSUInteger i = 0, max = array.count; i < max; i++) {
                 NSObject *obj = array[i];
                 [desc appendString:@"    "];
                     //生成每個元素的輸出信息,拼接字符串
                     [desc appendString:ModelDescriptionAddIndent(ModelDescription(obj).mutableCopy, 1)];
                     [desc appendString:(i + 1 == max) ? @"\n" : @";\n"];
                 }
                 [desc appendString:@"]"];
                 return desc;
             }
         }
    

    如果是自定義Model對象,則遍歷對象中每個屬性,獲取與之對應的_YYModelPropertyMeta對象,進一步判斷_YYModelPropertyMeta對象的類型,做不同的處理。如果屬于NSObject類型,遞歸調用ModelDescription方法。代碼如下:

    for (NSUInteger i = 0, max = properties.count; i < max; i++) {
     _YYModelPropertyMeta *property = properties[i];
     NSString *propertyDesc;
     if (property->_isCNumber) {
         NSNumber *num = ModelCreateNumberFromProperty(model, property);
             propertyDesc = num.stringValue;
     } else {
             switch (property->_type & YYEncodingTypeMask) {
             case YYEncodingTypeObject: {
                 id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, property->_getter);
                 propertyDesc = ModelDescription(v);
                     if (!propertyDesc) propertyDesc = @"<nil>";
                 } break;
     }
    }
    
  2. YYNSDateFromString方法,用來將字符串類型轉化成NSDate類型。因為NSDate是OC的對象,而JSON數據格式中不存在日期時間類型,因此需要進行轉化。通常是將具有固定格式的字符串轉成NSDate類型,例如:"yyyy-MM-dd HH:mm:ss"。

    該方法的主要思路是創建不同類型的NSDateFormatter對象和一組block,然后根據傳入的字符串參數的格式,執行相應的block,在block中再根據字符串參數調用相應的dateFormatter進行轉化。代碼注釋如下:

    dispatch_once(&onceToken, ^{
      {
         NSDateFormatter *formatter1 = [[NSDateFormatter alloc] init];
                formatter1.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
                formatter1.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
                formatter1.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss";
        ...
        blocks[19] = ^(NSString *string) {
                    if ([string characterAtIndex:10] == 'T') {
                        return [formatter1 dateFromString:string];
                    } else {
                        return [formatter2 dateFromString:string];
                    }
                };
        ...
        YYNSDateParseBlock parser = blocks[string.length]; //根據string選擇相應的block
        if (!parser) return nil;
        return parser(string); //執行block,選取dateFormatter進行轉化,輸出NSDate對象
    }
    

結尾

YYModel作為一個負責JSON數據和Model轉化的庫,十分易用和高效,特別是做了一些類型的兼容和轉化,避免了服務端接口數據類型和客戶端Model對象類型不兼容導致的問題,例如執行了不存在的方法而導致崩潰。另一方面,對YYModel的學習在一定程度也促進了對runtime機制的學習和了解。

關于YYModel的分析到這兒先告一段落,由于本人的iOS基礎有待提升,再加上表達能力有限,文中許多地方的分析和思路,表達的不是很準確和清楚,希望通過今后的學習和練習,提升自己的水平。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • 導語:YYModel庫是優秀的模型轉換庫,可自動處理模型轉換(從JSON到Model 和 Model到JSON)的...
    南華coder閱讀 5,494評論 0 11
  • 如何集成? 支持CocoaPods,在 Podfile 中添加 pod 'YYModel'。 支持Carthage...
    松哥888閱讀 11,013評論 0 7
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • *7月8日上午 N:Block :跟一個函數塊差不多,會對里面所有的內容的引用計數+1,想要解決就用__block...
    炙冰閱讀 2,512評論 1 14