从YYModel源码看模型转换

导语:YYModel库是优秀的模型转换库,可自动处理模型转换(从JSON到Model 和 Model到JSON)的工作。在项目中基于YYModel打造QSBaseModel,本文主要分析源码,最后简单介绍QSBaseModel的情况。

一、概述

YYModel库可以实现 JSON转Model、Model转JSON、Model的序列化和反序列化等;在 JSON转Model过程中,可以设置属性名和JSON Key的映射、属性的黑白名单等。YYModel的具体使用可以参考YYModel,那里已经说得很详细了。

1、YYModel源码文件#####

源码文件总共5个,分别如下:

  • YYModel.h :YYMode库的头文件,在项目中#import"YYModel.h",就可以使用YYModel了
  • NSObject+YYModel.hNSObject+YYModel.m:NSObject的YYModel分类,YYModel的主要功能代码在这部分
  • YYClassInfo.h 和 **YYClassInfo.m ** :包含YYClassIvarInfo、YYClassMethodInfo、YYClassPropertyInfo、YYClassInfo等类的定义和实现。将Model的成员变量、方法、成员属性以及类等信息抽象成YYClassIvarInfo(成员变量)、YYClassMethodInfo(方法 )、YYClassPropertyInfo(成员属性)、YYClassInfo(类)等对象。其中还定义了枚举类型YYEncodingType,列举了各类编码信息,占据1个字节,包括值类型(YYEncodingTypeMask = 0xFF) 、方法限定类型(YYEncodingTypeQualifierMask = 0xFF00)、属性修饰类型(YYEncodingTypePropertyMask = 0xFF0000 )。
2、JSON->Model基本流程#####

类方法 + (instancetype)yy_modelWithJSON:函数是解析JSON的入口。从它内部的函数调用链,可以发现解析流程主要分三步:

1)将JSON转换成了字典

//NSObject (YYModel) - 解析JSON的入口函数
+ (instancetype)yy_modelWithJSON:(id)json {
    NSDictionary *dic = [self _yy_dictionaryWithJSON:json];
    //....
}

//NSObject (YYModel) - 真正将JSON转字典的函数
+ (NSDictionary *)_yy_dictionaryWithJSON:(id)json {
    //...
    return dic;
}

2)根据类对象,获取YYModelMeta对象

YYModelMeta是描述Model的实例对象信息非常重要的类

 //NSObject (YYModel) - 解析JSON的入口函数
+ (instancetype)yy_modelWithJSON:(id)json {
    //....
    return [self yy_modelWithDictionary:dic];
}

//NSObject (YYModel)  - 获得YYModelMeta对象
+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
     //...   
    //先获取当前类对象,然后根据类对象获得**YYModelMeta对象**
    Class cls = [self class];
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
    if (modelMeta->_hasCustomClassFromDictionary) {
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }
    //...
}

说明2-1:modelCustomClassForDictionary:函数在获得YYModelMeta对象之后,可以修改解析数据对应的Class,一般用来修改Model属性值对应的Class。

 // _YYModelMeta -  获取和缓存了YYModelMeta对象
 + (instancetype)metaWithClass:(Class)cls {
    //...
    meta = [[_YYModelMeta alloc] initWithClass:cls];
    //...
}

 // _YYModelMeta - 真正将类对象 转成  _YYModelMeta对象的函数
- (instancetype)initWithClass:(Class)cls {
    //获取类信息对象
    YYClassInfo *classInfo = [YYClassInfo classInfoWithClass:cls];
    //实例化_YYModelMeta其他成员变量....
 }

说明2-2:YYModelMeta对象包含类信息YYClassInfo对象、解析数据等信息。而YYClassInfo对象中又包含方法列表、成员变量列表和属性列表。这些信息都是JSON解析成Model所必须的。

3)解析字典,获取Model

//NSObject (YYModel) 
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
        //...
        if (modelMeta->_hasCustomTransformFromDictionary) {
            return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
        }
        return YES;
}

说明3-1:modelCustomTransformFromDictionary:函数可以在JSON 转换成Model 后调用,在该函数实现中,可以对数据进行校验,校验不通过,返回 NO,则该 Model 会被忽略;也可以处理YYModel自动转换不能完成的工作。

//静态函数
static void ModelSetValueForProperty(__unsafe_unretained id model,
                                 __unsafe_unretained id value,
                                 __unsafe_unretained _YYModelPropertyMeta *meta) {
      //...
}

说明3-2:ModelSetValueForProperty是真正处理数据解析,为属性赋值的函数。

总结:YYModel中JSON解析的关键阶段在于:获取YYModelMeta对象 和 解析字典。为此,YYModel设计了YYModelMeta、_YYModelPropertyMeta、YYEncodingType、YYClassInfo、YYClassIvarInfo、YYClassMethodInfo、YYClassPropertyInfo等一系列类。

3、Model -> JSON的基本流程#####

实例方法- (NSString *)yy_modelToJSONObject是Model转JSON的入口函数,从它内部的函数调用链,发现调用ModelToJSONObjectRecursive来将Model转成id对象。最后只返回属于NSArray或NSDictionary类型的id对象。

- (id)yy_modelToJSONObject {
    id jsonObject = ModelToJSONObjectRecursive(self);
    if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject;
    if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject;
        return nil;
 }

说明1:Model -> JSON的关键在于:递归调用ModelToJSONObjectRecursive函数,实现对model各个属性的的转换,最终生成只包含NSArray/NSDictionary/NSString/NSNumber/NSNull的JSON对象。

说明2:通过yy_modelToJSONObject得到JSON对象之后,还可以通过NSJSONSerialization的dataWithJSONObject函数将JSONObject为NSData对象,这也解释了为什么JSONObject必须是NSArray或NSDictionary类型,最后将NSData对象转成NSString对象,即JSON字符串。

二、核心类之YYModelMeta

YYModelMeta通过- (instancetype)initWithClass:(Class)cls方法初始化YYModelMeta对象,传入的是Model的Class。

1、YYModelMeta的成员变量
@interface _YYModelMeta : NSObject {
    @package
    //类信息对象
    YYClassInfo *_classInfo;
    //所有属性的信息, key为键,propertyMeta为值
    NSDictionary *_mapper;
    // 所有属性的信息
    NSArray *_allPropertyMetas;
    // 映射到keyPath属性的信息
    NSArray *_keyPathPropertyMetas;
    //映射到一个数组的属性信息
    NSArray *_multiKeysPropertyMetas;
    //所有属性映射的个数
    NSUInteger _keyMappedCount;
    //model的类型
    YYEncodingNSType _nsType;

    BOOL _hasCustomWillTransformFromDictionary;  //是否实现了modelCustomWillTransformFromDictionary函数
    BOOL _hasCustomTransformFromDictionary; //是否实现了modelCustomTransformFromDictionary函数
    BOOL _hasCustomTransformToDictionary; //是否实现了modelCustomTransformToDictionary函数
    BOOL _hasCustomClassFromDictionary; //是否实现了modelCustomClassForDictionary函数
}

说明1-1:_mapper存放的是所有以key为键,_YYModelPropertyMeta对象为值的键值对;_allPropertyMetas、_keyPathPropertyMetas、_multiKeysPropertyMetas这些数组结构中存放的都是_YYModelPropertyMeta对象。

说明1-2:如果JSON key是使用keyPath来描述的,那么_keyPathPropertyMetas保存的是映射到keyPath属性的信息。

说明1-3:如果一个属性对应多个JSON key,那么_multiKeysPropertyMetas中保存的是映射到一个数组的属性信息。

说明1-4:Model属性的信息被封装在YYModelPropertyMeta对象中。

2、YYModelMeta的初始化
 // YYModelMeta - 根据Model的类对象获取类信息对象classInfo,然后根据classInfo去初始化成员变量。
- (instancetype)initWithClass:(Class)cls初始化YYModelMeta{
      //
      YYClassInfo *classInfo = [YYClassInfo classInfoWithClass:cls];
      if (!classInfo) return nil;
      self = [super init];
      //....
}

在该函数中,init过程如下:

1)根据Class获取YYClassInfo实例

2)获取黑名单(黑名单中的属性被忽略,实现+ (NSArray *)modelPropertyBlacklist)

3)获取白名单(白名单之外的属性都被忽略,实现 + (NSArray *)modelPropertyWhitelist)

4)获取类型为集合的属性中存储的类类型(实现+ (NSDictionary *)modelContainerPropertyGenericClass)

5)遍历Model类及其父类(父类不包括NSObject)的属性信息。在遍历过程中将YYClassPropertyInfo对象转成_YYModelPropertyMeta对象,根据2)、3)、4)获取的信息,设置_allPropertyMetas属性。

6)获取属性映射字典,如果没有没有指定映射或者说还有部分属性没有指定映射,就认为JSON中key(mappedToKey)和属性名是一样的,并据此设置_mapper、_keyPathPropertyMetas、_multiKeysPropertyMetas、_keyMappedCount属性。

说明2-1:通过_YYModelMeta对象的初始化,可了解到我们为Model类设置的属性映射、属性容器类中数据类型、黑白名单等方法,是在这里发挥作用,影响Model各个属性的赋值的。

三、核心类之YYClassInfo####

YYClassInfo描述Model类 类信息 的类

1、YYClassInfo的属性信息#####
@interface YYClassInfo : NSObject  
@property (nonatomic, assign, readonly) Class cls; ///< class object
@property (nullable, nonatomic, assign, readonly) Class superCls; ///< super class object
@property (nullable, nonatomic, assign, readonly) Class metaCls;  ///< class's meta class object
@property (nonatomic, readonly) BOOL isMeta; ///< whether this class is meta class
@property (nonatomic, strong, readonly) NSString *name; ///< class name
@property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< super class's class info
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; ///< ivars
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< methods
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; ///< properties
@end

说明1-1:YYModel将成员变量、方法、成员属性以及类这四类信息,从C层面的函数调用抽象成OC的类,这些类分别是YYClassIvarInfo(成员变量)、YYClassMethodInfo(方法 )、YYClassPropertyInfo(成员属性)、YYClassInfo(类)。

说明1-2:YYClassInfo中 成员变量信息ivarInfos、方法信息methodInfos、属性信息propertyInfos分别存放三者的name为键,以后三者为值的字典,由于YYModel使用遍历属性的方式来达到模型转换的目的,所以其中的propertyInfos起比较重要的作用。

2、YYClassInfo对象的初始化#####

在YYModelMeta的初始化中是调用classInfoWithClass:来获取YYClassInfo对象的,然而真正初始化的地方在initWithClass函数

//YYClassInfo-获得和缓存了YYClassInfo对象
+ (nullable instancetype)classInfoWithClass:(Class)cls{
   if (!info) {
      info = [[YYClassInfo alloc] initWithClass:cls];
      //...
    }
    //...
    return info;
}

//YYClassInfo-真正的初始化函数
- (instancetype)initWithClass:(Class)cls {
    if (!cls) return nil;
    self = [super init];
    _cls = cls;
    _superCls = class_getSuperclass(cls);
    _isMeta = class_isMetaClass(cls);
    if (!_isMeta) {
        _metaCls = objc_getMetaClass(class_getName(cls));
    }
    _name = NSStringFromClass(cls);
    [self _update];

    _superClassInfo = [self.class classInfoWithClass:_superCls];
    return self;
}
//更新ivarInfos、propertyInfos、methodInfos等属性值
- (void)_update{
      //...
}

在该函数中,init过程如下:

1)根据Class设置_cls、_superCls、_isMeta等成员变量。

2)调用私有函数_update中。设置ivarInfos、propertyInfos、methodInfos等属性(最重要)。

3)设置其他属性。

说明2-1:_update函数中,将runtime得到的成员变量、属性和方法信心 分别 封装成YYClassIvarInfo对象、YYClassPropertyInfo对象和YYClassMethodInfo对象,然后以他们的名字为key,对象为值,存入对应的ivarInfos、propertyInfos、methodInfos这些字典中。

四、核心类之_YYModelPropertyMeta

在YYModelMeta的初始化中,很重要的工作是,将classInfo中的属性信息propertyInfos中每一个YYClassPropertyInfo对象转成_YYModelPropertyMeta对象。_YYModelPropertyMeta是对Model类属性信息的描述类。

1、_YYModelPropertyMeta的成员变量
@interface _YYModelPropertyMeta : NSObject {
    @package
    NSString *_name;             ///< property's name
    YYEncodingType _type;        ///< property's type
    YYEncodingNSType _nsType;    ///< property's Foundation type
    BOOL _isCNumber;             ///< is c number type
    Class _cls;                  ///< property's class, or nil
    Class _genericCls;           ///< container's generic class, or nil if threr's no generic class
    SEL _getter;                 ///< getter, or nil if the instances cannot respond
    SEL _setter;                 ///< setter, or nil if the instances cannot respond
    BOOL _isKVCCompatible;       ///< YES if it can access with key-value coding
    BOOL _isStructAvailableForKeyedArchiver; ///< YES if the struct can encoded with keyed archiver/unarchiver
    BOOL _hasCustomClassFromDictionary; ///< class/generic class implements +modelCustomClassForDictionary:

    /*
     property->key:       _mappedToKey:key     _mappedToKeyPath:nil            _mappedToKeyArray:nil
     property->keyPath:   _mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil
     property->keys:      _mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath    _mappedToKeyArray:keys(array)
     */
    NSString *_mappedToKey;      ///< the key mapped to
    NSArray *_mappedToKeyPath;   ///< the key path mapped to (nil if the name is not key path)
    NSArray *_mappedToKeyArray;  ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys)
    YYClassPropertyInfo *_info;  ///< property's info
    _YYModelPropertyMeta *_next; ///< next meta if there are multiple properties mapped to the same key.
}
@end

说明1:_YYModelPropertyMeta的成员变量中包括属性类型_type、_nsType、属性Class、集合类中的Class(_genericCls)、setter和getter方法等。

说明2:_mappedToKey是映射到的key(@{property : key});_mappedToKeyPath映射到的keyPath(@{property : key1.key2})
_mappedToKeyArray映射到的数组(@{property : @[key1, key2]})。而每个_YYModelPropertyMeta中,这三者只有其中一个会有值。有了这三个属性,就可以获取需要转化的对应字典的value了。

2、_YYModelPropertyMeta的初始化函数
//_YYModelPropertyMeta
+ (instancetype)metaWithClassInfo:(YYClassInfo *)classInfo propertyInfo:(YYClassPropertyInfo *)propertyInfo generic:(Class)generic {
     //...
    return meta;
}

说明2-1:在该函数中,根据propertyInfo对象和generic信息初始化_YYModelPropertyMeta中各个成员变量。其中重要是属性Foundation类型_nsType的赋值。由于YYModel将属性的类型大致分为三类:C数值类型、NS系统自带基本类类型 以及 非常规类型(如CGSize等结构体),框架中提供一些函数判断类型,方便后面的属性赋值处理。

1)如果属性类型是NS系统类型,更新_nsType属性值,具体的类型通过调用YYClassGetNSType来判断。源码如下:

static force_inline YYEncodingNSType YYClassGetNSType(Class cls) {
    if (!cls) return YYEncodingTypeNSUnknown;
    if ([cls isSubclassOfClass:[NSMutableString class]]) return YYEncodingTypeNSMutableString;
    if ([cls isSubclassOfClass:[NSString class]]) return YYEncodingTypeNSString;
    if ([cls isSubclassOfClass:[NSDecimalNumber class]]) return YYEncodingTypeNSDecimalNumber;
    if ([cls isSubclassOfClass:[NSNumber class]]) return YYEncodingTypeNSNumber;
    if ([cls isSubclassOfClass:[NSValue class]]) return YYEncodingTypeNSValue;
    if ([cls isSubclassOfClass:[NSMutableData class]]) return YYEncodingTypeNSMutableData;
    if ([cls isSubclassOfClass:[NSData class]]) return YYEncodingTypeNSData;
    if ([cls isSubclassOfClass:[NSDate class]]) return YYEncodingTypeNSDate;
    if ([cls isSubclassOfClass:[NSURL class]]) return YYEncodingTypeNSURL;
    if ([cls isSubclassOfClass:[NSMutableArray class]]) return YYEncodingTypeNSMutableArray;
    if ([cls isSubclassOfClass:[NSArray class]]) return YYEncodingTypeNSArray;
    if ([cls isSubclassOfClass:[NSMutableDictionary class]]) return YYEncodingTypeNSMutableDictionary;
    if ([cls isSubclassOfClass:[NSDictionary class]]) return YYEncodingTypeNSDictionary;
    if ([cls isSubclassOfClass:[NSMutableSet class]]) return YYEncodingTypeNSMutableSet;
    if ([cls isSubclassOfClass:[NSSet class]]) return YYEncodingTypeNSSet;
    return YYEncodingTypeNSUnknown;
}

说明2-2:由于类簇的原因,我们是无法在runtime时获取属性是否是mutable的,所以需要先判断是否为mutable。

**2) **调用YYEncodingTypeIsCNumber判断属性的类型是否是C数值类型

static force_inline BOOL YYEncodingTypeIsCNumber(YYEncodingType type) {
    switch (type & YYEncodingTypeMask) {
        case YYEncodingTypeBool:
        case YYEncodingTypeInt8:
        case YYEncodingTypeUInt8:
        case YYEncodingTypeInt16:
        case YYEncodingTypeUInt16:
        case YYEncodingTypeInt32:
        case YYEncodingTypeUInt32:
        case YYEncodingTypeInt64:
        case YYEncodingTypeUInt64:
        case YYEncodingTypeFloat:
        case YYEncodingTypeDouble:
        case YYEncodingTypeLongDouble: return YES;
        default: return NO;
    }
}

说明2-3:将BOOL值结果赋值给_isCNumber,方便后面属性赋值时候的处理。

五、Dictionary -> JSON (解析关键)####

在解析之前,获取了YYModelMeta对象,调用yy_modelSetWithDictionary去处理解析。

1、从yy_modelSetWithDictionary看解析的过程#####
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {

    if (!dic || dic == (id)kCFNull) return NO;
    if (![dic isKindOfClass:[NSDictionary class]]) return NO;

    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
    if (modelMeta->_keyMappedCount == 0) return NO;

    //可以修改dic的值
    if (modelMeta->_hasCustomWillTransformFromDictionary) {
        dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
        if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    }

    //创建集合上下文
    ModelSetContext context = {0};
    context.modelMeta = (__bridge void *)(modelMeta);
    context.model = (__bridge void *)(self);
    context.dictionary = (__bridge void *)(dic);

    if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
        CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
        if (modelMeta->_keyPathPropertyMetas) {
            CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
                               CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                             ModelSetWithPropertyMetaArrayFunction,
                             &context);
        }
        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);
    }

    if (modelMeta->_hasCustomTransformFromDictionary) {
        return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
    }
    return YES;
}

该函数处理流程:

1)如果实现modelCustomWillTransformFromDictionary:这个函数,就按照实现方法修改解析的Dic的数据;

2)创建了一个模型集合上下文context,存放了modelMeta对象、model对象、解析的数据字典dictionary等数据,将context交给ModelSetWithPropertyMetaArrayFunction函数去解析(给model赋值);

3)解析完成后,可以实现modelCustomTransformFromDictionary方法,对model的属性进行更改。

说明1-1: 由于modelCustomWillTransformFromDictionary和modelCustomTransformFromDictionary函数都是交给开发去自定义实现的,不是框架处理的事情,我们关注源码中是如何解析字典的。

说明1-2:当modelMeta->_keyMappedCount大于等于CFDictionaryGetCount((CFDictionaryRef)dic)的时候,遍历字典,设置并以字典为基准,设置模型中与字典相对应的属性(ModelSetWithDictionaryFunction函数),如果_keyPathPropertyMetas不为空,设置映射到keyPath的属性,如果_multiKeysPropertyMetas不为空,设置映射到数组的属性。否则直接通过_allPropertyMetas设置所有属性。

2、ModelSetWithDictionaryFunction函数功能#####

在yy_modelSetWithDictionary函数中,只有当modelMeta->_keyMappedCount大于等于CFDictionaryGetCount((CFDictionaryRef)dic)时,才调用ModelSetWithDictionaryFunction函数,其实现如下:

//字典回调函数
static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
    ModelSetContext *context = _context;
    __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
    __unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)];
    __unsafe_unretained id model = (__bridge id)(context->model);
    while (propertyMeta) {
        // 映射到同个key之后,这里循环赋给属性相同的值
        if (propertyMeta->_setter) {
            ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
        }
        propertyMeta = propertyMeta->_next;
    };
}

该函数处理流程:

1)根据字典的key从_mapper中获取对应的_YYModelPropertyMeta

2) 调用ModelSetValueForProperty设置属性值。

3)如果propertyMeta的_next不为空,即表示有多个属性被映射到了同一个key。这样只需要从字典中取一次value,就可以设置被映射到同一个key的所有属性。

3、ModelSetWithPropertyMetaArrayFunction函数功能#####

通过_allPropertyMetas设置时,则需要对每个属性都对字典做一次取值操作

//数组回调函数
static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) {
    ModelSetContext *context = _context;
    __unsafe_unretained NSDictionary *dictionary = (__bridge NSDictionary *)(context->dictionary);
    __unsafe_unretained _YYModelPropertyMeta *propertyMeta = (__bridge _YYModelPropertyMeta *)(_propertyMeta);
    if (!propertyMeta->_setter) return;
    id value = nil;

    if (propertyMeta->_mappedToKeyArray) {
         // 映射到多个key
        value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray);
    } else if (propertyMeta->_mappedToKeyPath) {
        // 映射到keyPath
        value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath);
    } else {
       // 映射到一个key
        value = [dictionary objectForKey:propertyMeta->_mappedToKey];
    }

    if (value) {
        __unsafe_unretained id model = (__bridge id)(context->model);
        ModelSetValueForProperty(model, value, propertyMeta);
    }
}

该函数处理流程:

1)如果一个属性映射到多个JSON key(propertyMeta->_mappedToKeyArray不为空),那么只取第一个匹配成功的key,后续的key将会被略过。

static force_inline id YYValueForMultiKeys(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *multiKeys) {
    id value = nil;
    for (NSString *key in multiKeys) {
        if ([key isKindOfClass:[NSString class]]) {
            value = dic[key];
            if (value) break;
        } else {
            value = YYValueForKeyPath(dic, (NSArray *)key);
            if (value) break;
        }
    }
    return value;
}
  1. 如果映射关系中,通过key1.key2来描述JSON Key(propertyMeta->_mappedToKeyPath不为空),那么将映射的keyPath以.为分隔符拆分成多个字符串,并以数组的形式存储,最终用循环获取value的方式代替valueForKeyPath:避免从非字典取value时发生崩溃。

    static force_inline id YYValueForKeyPath(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *keyPaths) {
    id value = nil;
    for (NSUInteger i = 0, max = keyPaths.count; i < max; i++) {
    value = dic[keyPaths[i]];
    if (i + 1 < max) {
    if ([value isKindOfClass:[NSDictionary class]]) {
    dic = value;
    } else {
    return nil;
    }
    }
    }
    return value;
    }

3)获取value的值,然后交给ModelSetValueForProperty去设置对应属性的值。

4、真正设置属性值ModelSetValueForProperty函数#####

在ModelSetValueForProperty中,为属性赋值分为三步:

1) 处理属性类型是C的数值类型的情况

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

2) 处理属性类型是NS系统定义类型的情况

如果是属性类型是如NSString、NSNumber这样的非集合类、采用objc_msgSend直接调用setter,给属性赋值;如果是集合类,处理比较麻烦,以属性类型是NSArray或NSMutableArray(_nsType是YYEncodingTypeNSArray或YYEncodingTypeNSMutableArray)的处理源码为例:

case YYEncodingTypeNSArray:
case YYEncodingTypeNSMutableArray: {
     if (meta->_genericCls) {
        NSArray *valueArr = nil;
        if ([value isKindOfClass:[NSArray class]]) valueArr = value;
        else if ([value isKindOfClass:[NSSet class]]) valueArr = ((NSSet *)value).allObjects;
        if (valueArr) {
              NSMutableArray *objectArr = [NSMutableArray new];
              for (id one in valueArr) {
              // 已经是所要对象了
              if ([one isKindOfClass:meta->_genericCls]) {
                  [objectArr addObject:one];
              } else if ([one isKindOfClass:[NSDictionary class]]) {
                  // 给的是字典,要自己构造
                  Class cls = meta->_genericCls;
                  if (meta->_hasCustomClassFromDictionary) {
                      // 由字典返回对应的类(透传) <<< 由开发者实现
                        cls = [cls modelCustomClassForDictionary:one];
                      if (!cls) cls = meta->_genericCls; // for xcode code coverage
                  }
                  NSObject *newOne = [cls new];
                  // 根据获得的类,创建实例
                  [newOne yy_modelSetWithDictionary:one];
                  if (newOne) [objectArr addObject:newOne];
              }
        }
        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, objectArr);
    }
} else {
      if ([value isKindOfClass:[NSArray class]]) {
          if (meta->_nsType == YYEncodingTypeNSArray) {
              ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
        } else {
            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                       meta->_setter,
                                                       ((NSArray *)value).mutableCopy);
        }
      } else if ([value isKindOfClass:[NSSet class]]) {
          if (meta->_nsType == YYEncodingTypeNSArray) {
            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSSet *)value).allObjects);
          } else {
              ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                       meta->_setter,
                                                       ((NSSet *)value).allObjects.mutableCopy);
          }
      }
}

1)在没有指定集合中的数据类型,即_genericCls为nil的情形下,如果value是NSArray或者NSSet类型,那么YYModel将value
直接赋给属性,不做任何解析。

2)在指定了集合中的数据类型,即_genericCls不为nil的情形下,会对每个元素进行解析并构造成相应的实例。如果集合元素依然是一个字典,那么就会调用yy_modelSetWithDictionary解析。在解析的过程中,可以实现modelCustomClassForDictionary:方法,重新指定集合中的数据类型。

3) 处理属性类型是非常规类型

非常规类型是C数值类型、NS定义的类型之外的类型,包括但不限于自定义Model类、CG结构体等。主要是自定义Model类,以解析自定义Model类为例,源码如下:

 case YYEncodingTypeObject: {
       if (isNull) {
            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
         } else if ([value isKindOfClass:meta->_cls] || !meta->_cls) {
            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)value);
        } else if ([value isKindOfClass:[NSDictionary class]]) {
            NSObject *one = nil;
            if (meta->_getter) {
                one = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter);
            }
            if (one) {
                [one yy_modelSetWithDictionary:value];
            } else {
                Class cls = meta->_cls;
                if (meta->_hasCustomClassFromDictionary) {
                    cls = [cls modelCustomClassForDictionary:value];
                    if (!cls) cls = meta->_genericCls; // for xcode code coverage
                }
                one = [cls new];
                [one yy_modelSetWithDictionary:value];
                 ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)one);
            }
        }
  } 

说明:如果属性是自定义Model类,通过Class实例化对象,然后调用yy_modelSetWithDictionary解析数据,给Model实例各个属性赋值。

六、Model -> JSONModel (重点)####

分析ModelToJSONObjectRecursive函数,可以看出根据Model的类型来处理的,Model的类型分:非集合类型、集合类型、自定义Model类

1、model是非集合类型的处理#####
//model是基本类型 : kCFNull、NSString、NSNumber或者nil,直接返回model
if (!model || model == (id)kCFNull) return model;
if (!model || model == (id)kCFNull) return model;
if ([model isKindOfClass:[NSString class]]) return model;
if ([model isKindOfClass:[NSNumber class]]) return model;

//model类型是NSURL、NSAttributedString、NSDate类型,转成字符串返回。是NSData就返回nil
if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString;
if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string;
if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model];
if ([model isKindOfClass:[NSData class]]) return nil;
2、model是集合类型的处理#####
//如果是NSDictionary,能JSON化就直接返回,否则调用ModelToJSONObjectRecursive递归处理,最后添加到字典中
if ([model isKindOfClass:[NSDictionary class]]) {
    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、NSArray,并且元素是基本类型就直接添加到数组中,否则调用ModelToJSONObjectRecursive嵌套解析成基本类型,然后添加到数组中
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;
}

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;
}
3、model是自定义Model类的处理#####

获取自定义Model类的modelMeta信息,然后遍历modelMeta._mapper,通过映射关系获取属性值,然后构造成字典。

1) 根据propertyMet对象提供的属性类型相关信息,处理属性值value,如果value为nil,放弃当前属性值的处理,去处理下一个属性值。

    if (!propertyMeta->_getter) return;
    
    id value = nil;
    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 {
        switch (propertyMeta->_type & YYEncodingTypeMask) {
            case YYEncodingTypeObject: {
                id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                value = ModelToJSONObjectRecursive(v);
                if (value == (id)kCFNull) value = nil;
            } break;
            case YYEncodingTypeClass: {
                Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                value = v ? NSStringFromClass(v) : nil;
            } break;
            case YYEncodingTypeSEL: {
                SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
                value = v ? NSStringFromSelector(v) : nil;
            } break;
            default: break;
        }
    }
    if (!value) return;

2)根据propertyMeta的_mappedToKeyPath或_mappedToKey构造字典

if (propertyMeta->_mappedToKeyPath) {
    NSMutableDictionary *superDic = dic;
    NSMutableDictionary *subDic = nil;
    for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) {
        NSString *key = propertyMeta->_mappedToKeyPath[i];
        if (i + 1 == max) { // end  { ext = { d = Apple; }; }, 最后的key才赋值, 即superDic[@"d"] = @"Apple"
            if (!superDic[key]) superDic[key] = value;
            break;
        }

        subDic = superDic[key];
        if (subDic) {
            // 说明这一层字典已经有键值对了
            if ([subDic isKindOfClass:[NSDictionary class]]) {
                // 拷贝成可变的(没这一句也可,因为刚开始时创建的都是NSMutableDictionary), 方便i + 1 == max时进行赋值
                subDic = subDic.mutableCopy;
                superDic[key] = subDic;
            } else {
                break;
            }
        } else {
            // key下没有value,创建可变字典赋给当前的key
            subDic = [NSMutableDictionary new];
            superDic[key] = subDic;
        }
        // 最顶层的字典(@{@"a" : @{@"b" : @"c"}},即字典@{@"b" : @"c"})
        superDic = subDic;
        subDic = nil;
    }
} else {
    if (!dic[propertyMeta->_mappedToKey]) {
        dic[propertyMeta->_mappedToKey] = value;
    }
}

七、QSBaseModel封装

1、为什么要封装YYModel

1)YYModel很强大,可以通过实现NSObject+YYModel中的相关方法操作,干预模型转换的过程。由于项目中后台提供的数据接口格式固定,JSON解析中最大的需要是可以指定属性名和JSON Key的映射 以及 指定容器类中的数据类型,为了稍稍限制开发人员自由,让大家一起遵循统一的规范。

2)通过YYModel的相关方法重写Model类的Coding/Copying/hash/equal/description方法,只要Model类继承QSBaseModel就可以很方便地使用这些方法。

3)如果项目后期替换模型转换库,对业务也不会有什么影响。

2、QSBaseModel的定义和实现#####
//QSBaseModel.h
@interface QSBaseModel : NSObject<NSCoding,NSCopying>

+ (instancetype)modelFromJSON:(id)json;

- (NSString *)modelToJSONString;

@end

//QSBaseModel.m
#import "YYModel.h"
#import "QSBaseModel.h"

@implementation QSBaseModel

#pragma mark - 模型转换(public)
+ (instancetype)modelFromJSON:(id)json{
    return [self yy_modelWithJSON:json];
}

- (NSString *)modelToJSONString {
    return [self yy_modelToJSONString];
}

#pragma mark - 序列化和反序列化
- (void)encodeWithCoder:(NSCoder *)aCoder{
    [self yy_modelEncodeWithCoder:aCoder];
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
    return [self yy_modelInitWithCoder:aDecoder];
}

#pragma mark - 实现copy方法(实现深拷贝)
- (id)copyWithZone:(NSZone *)zone {
    return [self yy_modelCopy];
}

#pragma mark -重写hash、isEqual:和description方法
- (NSUInteger)hash {
    return [self yy_modelHash];
}

- (BOOL)isEqual:(id)object {
    return [self yy_modelIsEqual:object];
}

- (NSString *)description{
    return [self yy_modelDescription];
}

@end

说明1:在指定属性名和JSON Key的映射时,原则上不使用keyPath描述JSON Key。

说明2:因为YYModel中将JSON转成Model的过程是,先生成Model实例,再用JSON数据给Model的属性赋值,所以会出现Model中部分属性值是nil的情况,使用这些Model的属性值前需要简单校验。主要是对属性类型是字符串、数组、字典、集合的校验。下面是校验的宏定义。

#define ISVALID_STRING(x)                       (x && [x isKindOfClass:[NSString class]] && [x length])
#define ISVALID_ARRAY(x)                        (x && [x isKindOfClass:[NSArray class]] && [x count])
#define ISVALID_DICTIONARY(x)                   (x && [x isKindOfClass:[NSDictionary class]] && [x count])
#define ISVALID_SET(x)                          (x && [x isKindOfClass:[NSSet class]] && [x count])

结束####

参考资料
YYModel
YYModel阅读小记

其他

这里只是简单记录了YYModel模型转换的过程,YYModel有很多值得学习的地方,后面再谈

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

推荐阅读更多精彩内容