《YYModel源码分析(二)NSObject+YYModel》

承接上文《YYModel源码分析(一)YYClassInfo》 以前文章讲述了YYClassInfo如何将runtime类结构封装到OC层。这篇文章主要讲述YYModel是如何用NSObject分类,实现非侵入式json-model的(类型转换,容错,model转json会在其余文章中讨论)。json

写在开头

NSObject+ YYModel中并不仅有NSObject分类,还包含了_YYModelPropertyMeta_YYModelMeta以及协议<YYModel>,固然又声明了不少静态(内联)函数,至于为何用内联函数而不用类方法或者宏定义,是由于内联函数在编译中会将代码插入到调用的位置,这样会提升调用效率,相对于宏又有函数的特色。具体能够看这里《IOS 内联函数Q&A》数组

协议

首先字典转模型,就是字典中key对应的value赋值给model对应的属性的过程,默认状况下咱们都会将属性名对应成字典的key,那么若是咱们不想这么起名字。或者咱们有这样一个json:bash

{
         "n":"Harry Pottery",
         "p": 256,
         "ext" : {
             "desc" : "A book written by J.K.Rowling."
         },
         "ID" : 100010
 }
复制代码

咱们想赋值给这个modelapp

@interface YYBook : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
复制代码

要实现以上的需求就必须告诉YYModel属性应该如何取值,<YYModel>提供了这样一套规范协议。接下来咱们依次看一下函数

/**
 返回一个map,key是属性名,value是json中对应的key,能够有三种形式。
 
 @{@"name"  : @"n",                         //对应一个json中的key
   @"desc"  : @"ext.desc",                  //对应一个json地址。
   @"bookID": @[@"id", @"ID", @"book_id"]}; //对应多个json中的key。
 */
+ (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper;
/**
 告诉YYModel容器类型中元素的类型。以下:
 @{@"shadows" : [YYShadow class],
   @"borders" : YYBorder.class,
   @"attachments" : @"YYAttachment" }
 value能够穿Class也能够穿字符串,能够自动解析
 */
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
/**
想根据dictionary提供的数据建立不一样的类,实现这个方法,会根据返回的类型建立对象
注意这个协议对`+modelWithJSON:`, `+modelWithDictionary:`,这两个方法有效
 */
+ (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;
/**
 在json转model的时候,黑名单上的属性都会被忽略
 */
+ (nullable NSArray<NSString *> *)modelPropertyBlacklist;
/**
 在json转model的时候,若是属性没有在白名单上,将会被忽略。
 */
+ (nullable NSArray<NSString *> *)modelPropertyWhitelist;
/**
 这个方法能够在json转model以前对dic进行更改,json转model将按照返回的dic为准。
 */
- (NSDictionary *)modelCustomWillTransformDictionary:(NSDictionary *)dic;
/**
 该接口会在json转model以后调用,用于不适合模型对象时作额外的逻辑处理。咱们也能够用这个接口来验证模型转换的结果
 */
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic;
复制代码

静态函数

在NSObject+YYModel.m文件中一看,差很少一半都是静态(内联)函数,内联函数咱们前面已经说过了,static修饰函数跟普通函数有如下区别:源码分析

  • 语法与C++保持一致,只在模块内部可见
  • 跟类无关,因此也没法调用self,只能根据参数实现相关功能
  • 静态参数不参与动态派发,没有再函数列表里,静态绑定 因此由于要频繁调用,因此寻求更高效的static函数。我把静态函数和其功能都列在下面了,供参考。
//将类解析成Foundation类型,传入Class返回枚举YYEncodingNSType
static force_inline YYEncodingNSType YYClassGetNSType(Class cls) 
//经过YYEncodingType判断是不是c数字类型
static force_inline BOOL YYEncodingTypeIsCNumber(YYEncodingType type)
//将一个ID类型的数据解析成NSNumber,这里主要处理了字符串转数字的状况
static force_inline NSNumber *YYNSNumberCreateFromID(__unsafe_unretained id value)
//NSString类型数据转NSDate,这里几乎兼容了全部时间格式,而且作了容错
static force_inline NSDate *YYNSDateFromString(__unsafe_unretained NSString *string)
//获取NSBlock这个类,加入了打印咱们能够看出 block 的父类的关系是block -------> NSGlobalBlock ---------> NSBlock
static force_inline Class YYNSBlockClass() 
//获取ISO时间格式
static force_inline NSDateFormatter *YYISODateFormatter()
//根据KeyPath获取一个字典中的数据
static force_inline id YYValueForKeyPath(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *keyPaths) 
//一句多个Key从字典中获取数据,这里若是有一个Key有值就取值返回。
static force_inline id YYValueForMultiKeys(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *multiKeys) 
//
static force_inline NSNumber *ModelCreateNumberFromProperty(__unsafe_unretained id model,
                                                            __unsafe_unretained _YYModelPropertyMeta *meta)
//为一个对象设置数值属性
static force_inline void ModelSetNumberToProperty(__unsafe_unretained id model,
                                                  __unsafe_unretained NSNumber *num,
                                                  __unsafe_unretained _YYModelPropertyMeta *meta)
//为对象的属性赋值
static void ModelSetValueForProperty(__unsafe_unretained id model,
                                     __unsafe_unretained id value,
                                     __unsafe_unretained _YYModelPropertyMeta *meta)
//经过键值为_context设置属性,_context是一个结构体,后面咱们会讲到,包含了数据源dic、model和_YYModelMeta。
static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context)
//为对象的_propertyMeta属性赋值。
static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) 
//由model返回一个有效的json。
static id ModelToJSONObjectRecursive(NSObject *model) 
复制代码

关于这些方法的实现,后面用到会细说。测试

_YYModelPropertyMeta

其实_YYModelPropertyMeta类型是在YYClassPropertyInfo的基础上的进一步解析而且关联了从<YYModel>协议中的取值信息。ui

/// A property info in object model.
@interface _YYModelPropertyMeta : NSObject {
    @package
    NSString *_name;             ///< 属性名
    YYEncodingType _type;        ///< 属性类型,OC类型统一为YYEncodingTypeObject
    YYEncodingNSType _nsType;    ///< 属性的Foundation类型,NSString等等。
    BOOL _isCNumber;             ///< 是不是c数字类型
    Class _cls;                  ///< 属性类型,
    Class _genericCls;           ///< 若是是容器类型,是容器类型内元素的类型,若是不是容器类型为nil。
    SEL _getter;                 ///< getter方法
    SEL _setter;                 ///< setter方法
    BOOL _isKVCCompatible;       ///< 是否可使用KVC
    BOOL _isStructAvailableForKeyedArchiver; ///< 结构体是否支持归档解挡
    BOOL _hasCustomClassFromDictionary; ///< 是否实现了 +modelCustomClassForDictionary:协议
    
    NSString *_mappedToKey;      ///< 代表该属性取数据源中_mappedToKey对应的value的值。
    NSArray *_mappedToKeyPath;   ///< 代表该属性取数据源中_mappedToKeyPath对应路径的value值,若是为nil说明没有关键路径
    NSArray *_mappedToKeyArray;  ///< key或者keyPath的数组,代表可从多个key中取值。
    YYClassPropertyInfo *_info;  ///< 属性信息
    _YYModelPropertyMeta *_next; ///< 下一个元数据,若是有多个属性映射到同一个键。
}
@end
复制代码

_YYModelPropertyMeta属性咱们能够看出,若是属性是Foundation类型,会被解析成具体的OC类型,用枚举的形式存储在_nstype中,同时由Model实现的<YYModel>协议能够获取到取值信息_mappedToKey_mappedToKeyPath _mappedToKeyArray信息,这个在以后的赋值操做中起着相当重要的做用。编码

@implementation _YYModelPropertyMeta

+ (instancetype)metaWithClassInfo:(YYClassInfo *)classInfo propertyInfo:(YYClassPropertyInfo *)propertyInfo generic:(Class)generic {
    // 这里有些许疑惑,generic是当属性是容器类时,容器类中包含的元素,代码逻辑是若是generic为空,且propertyInfo.protocols不为空,若是propertyInfo.protocols中的元素是Class的时候将此class赋值给generic,可是propertyInfo.protocols确实存储的是协议,propertyInfo.protocols的解析过程是取objc_property_attribute_t中<>中的字符,可是经测试只有一个属性遵循了某种协议才会出现<>字符,NSSArray<NSString*> *这样的属性编码字符串也是@"NSSArray",因此这块貌似没什么用。
    if (!generic && propertyInfo.protocols) {
        //
        for (NSString *protocol in propertyInfo.protocols) {
            Class cls = objc_getClass(protocol.UTF8String);
            if (cls) {
                generic = cls;
                break;
            }
        }
    }
    
    _YYModelPropertyMeta *meta = [self new];
    //给meta的成员变量赋值
    meta->_name = propertyInfo.name;
    //类型枚举
    meta->_type = propertyInfo.type;
    //存储属性元数据
    meta->_info = propertyInfo;
    //容器类包含的通用类型
    meta->_genericCls = generic;
    //若是属性是OC类型的
    if ((meta->_type & YYEncodingTypeMask) == YYEncodingTypeObject) {
        //解析成枚举
        meta->_nsType = YYClassGetNSType(propertyInfo.cls);
    } else {
        //判断是不是number类
        meta->_isCNumber = YYEncodingTypeIsCNumber(meta->_type);
    }
    //若是是结构图
    if ((meta->_type & YYEncodingTypeMask) == YYEncodingTypeStruct) {
        static NSSet *types = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSMutableSet *set = [NSMutableSet new];
            // 32 bit
            [set addObject:@"{CGSize=ff}"];
            [set addObject:@"{CGPoint=ff}"];
            [set addObject:@"{CGRect={CGPoint=ff}{CGSize=ff}}"];
            [set addObject:@"{CGAffineTransform=ffffff}"];
            [set addObject:@"{UIEdgeInsets=ffff}"];
            [set addObject:@"{UIOffset=ff}"];
            // 64 bit
            [set addObject:@"{CGSize=dd}"];
            [set addObject:@"{CGPoint=dd}"];
            [set addObject:@"{CGRect={CGPoint=dd}{CGSize=dd}}"];
            [set addObject:@"{CGAffineTransform=dddddd}"];
            [set addObject:@"{UIEdgeInsets=dddd}"];
            [set addObject:@"{UIOffset=dd}"];
            types = set;
        });
        //若是是以上结构体则支持归解档
        if ([types containsObject:propertyInfo.typeEncoding]) {
            meta->_isStructAvailableForKeyedArchiver = YES;
        }
    }
    meta->_cls = propertyInfo.cls;
    
    if (generic) {
        //容器类元素是否实现了 modelCustomClassForDictionary协议
        meta->_hasCustomClassFromDictionary = [generic respondsToSelector:@selector(modelCustomClassForDictionary:)];
    } else if (meta->_cls && meta->_nsType == YYEncodingTypeNSUnknown) {
        meta->_hasCustomClassFromDictionary = [meta->_cls respondsToSelector:@selector(modelCustomClassForDictionary:)];
    }
    
    //设置getter方法
    if (propertyInfo.getter) {
        if ([classInfo.cls instancesRespondToSelector:propertyInfo.getter]) {
            meta->_getter = propertyInfo.getter;
        }
    }
    //设置setter方法
    if (propertyInfo.setter) {
        if ([classInfo.cls instancesRespondToSelector:propertyInfo.setter]) {
            meta->_setter = propertyInfo.setter;
        }
    }
    
    if (meta->_getter && meta->_setter) {
        /*
         如下类型都不支持KVC
         */
        switch (meta->_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 YYEncodingTypeObject:
            case YYEncodingTypeClass:
            case YYEncodingTypeBlock:
            case YYEncodingTypeStruct:
            case YYEncodingTypeUnion: {
                meta->_isKVCCompatible = YES;
            } break;
            default: break;
        }
    }
    
    return meta;
}
@end
复制代码

_YYModelMeta

_YYModelMeta经过Model遵循的<YYModel>协议,收集取值信息,并映射到_YYModelPropertyMeta当中,将其中有效的信息封装到该类中。spa

@interface _YYModelMeta : NSObject {
    //@package当前framework可使用,外部不能够
    @package
    
    YYClassInfo *_classInfo;
    /// [key:_YYModelPropertyMeta]
    NSDictionary *_mapper;
    /// 全部的属性_YYModelPropertyMeta数据,这里包含当前类到跟类NSObject中的全部属性
    NSArray *_allPropertyMetas;
    /// 映射到KeyPath的属性_keyPathPropertyMetas集合
    NSArray *_keyPathPropertyMetas;
    /// 映射到多个键值的属性_keyPathPropertyMetas集合
    NSArray *_multiKeysPropertyMetas;
    /// 属性映射的数量。
    NSUInteger _keyMappedCount;
    /// Foundation类型
    YYEncodingNSType _nsType;
    
    BOOL _hasCustomWillTransformFromDictionary;
    BOOL _hasCustomTransformFromDictionary;
    BOOL _hasCustomTransformToDictionary;
    BOOL _hasCustomClassFromDictionary;
}
@end
复制代码

接下来讨论一下_YYModelMet是如何初始化的。过程以下

  • 1.从实现的modelPropertyBlacklist、modelPropertyWhitelist协议中获取取值黑名单、白名单。
  • 2.从实现的modelContainerPropertyGenericClass协议中获取容器类属性中的元素类型
  • 3.获取当前类及继承链直至NSObject中全部的属性生成_YYModelPropertyMeta对象,存储到allPropertyMetas
  • 4.从实现的modelCustomPropertyMapper协议中获取自定义map,这里map的key是属性名,value有三种状况,第一是对应一个取值key,第二是一个keypath用'.'隔开,第三是一个字符数组对应多个取值key
  • 5.遍历map,由mapkey取出对应的propertyMeta而后根据步骤4中value的三种状况给propertyMeta_mappedToKey、_mappedToKeyPath、_mappedToKeyArray赋值,这样就把属性和取值逻辑绑定在了一块儿
  • 6.给_keyMappedCount赋值,查看modelCustomWillTransformFromDictionary、modelCustomTransformFromDictionary、modelCustomTransformToDictionary 、modelCustomClassForDictionary这四个协议是否实现。

这个过程代码比较多,就不列出来了。感兴趣的能够本身看下哈。

NSObject (YYModel)

NSObject (YYModel)是YYModel非侵入式的关键,模型对象经过调用扩展方法实现json转model。接下来咱们用json-model的核心方法yy_modelWithDictionary举例。

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
    //容错处理
    if (!dictionary || dictionary == (id)kCFNull) return nil;
    if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;
    //获取当前类的类型
    Class cls = [self class];
    //建立_YYModelMeta
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
    //这里建立_YYModelMeta的目的就是查看是否实现了modelCustomClassForDictionary协议,哈哈,这里回溯一下modelCustomClassForDictionary的功能,这个协议你能够根据dictionary数据建立一个不一样于当前类的对象来完成json转model。
    if (modelMeta->_hasCustomClassFromDictionary) {
        //若是实现了这个协议则替换当前类型。
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }
    //由获取到的类型建立对象
    NSObject *one = [cls new];
    //调用yy_modelSetWithDictionary方法。
    if ([one yy_modelSetWithDictionary:dictionary]) return one;
    return nil;
}
复制代码

再看一下属性赋值的方法yy_modelSetWithDictionary

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
    //容错处理
    if (!dic || dic == (id)kCFNull) return NO;
    if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    //建立_YYModelMeta
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
    if (modelMeta->_keyMappedCount == 0) return NO;
    //查看是否实现modelCustomWillTransformFromDictionary协议,若是实现调用该方法,处理dic
    if (modelMeta->_hasCustomWillTransformFromDictionary) {
        dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
        if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    }
    //建立ModelSetContext,一个结构体
    //    typedef struct {
    //        void *modelMeta;  ///< _YYModelMeta
    //        void *model;      ///< id (self)
    //        void *dictionary; ///< NSDictionary (json)
    //    } ModelSetContext;
    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意思是为字典中的每一个键值对调用一次函数
        CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
        if (modelMeta->_keyPathPropertyMetas) {
            //处理取值为_keyPathPropertyMetas形式的属性
            //CFArrayApplyFunction是为数组中的每一个元素对调用一次函数。
            CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
        if (modelMeta->_multiKeysPropertyMetas) {
            //处理取值为_multiKeysPropertyMetas形式的属性
            CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
    } else {
        //若是自定义键值数量小于数据源的键值数量,那么直接按照dic key值给属性赋值,自定义的无效
        CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
                             CFRangeMake(0, modelMeta->_keyMappedCount),
                             ModelSetWithPropertyMetaArrayFunction,
                             &context);
    }
    
    if (modelMeta->_hasCustomTransformFromDictionary) {
        return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
    }
    return YES;
}
复制代码

经过以上代码逻辑咱们知道,若是没有设置全量键值映射,也就是说实际数据源的键值数量大于自定义键值数量,那么自定义键值无效,会直接按照实际数据源的key对应属性名进行赋值。

咱们能够看到赋值操做中有两个比较重要的方法ModelSetWithDictionaryFunction,ModelSetWithPropertyMetaArrayFunction

/**
 经过键值给模型赋值
 
 @param _key     键
 @param _value   值
 @param _context 赋值必要的数据,model,modelMeta,dictionary
 */
static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
    ModelSetContext *context = _context;
    __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
    //经过key取到响应的属性
    __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;
    };
}
/**
 为模型的某一个属性赋值
 
 @param _propertyMeta 属性
 @param _context   赋值必要的数据,model,modelMeta,dictionary
 */
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) {
        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的方法,这个才是真正属性赋值的方法。这个函数作的就是经过runtime函数objc_msgSend调用对象的setter方法赋值,之因此代码量巨大是由于对全部的数据类型(c数字,foundation类型)作了判断并添加了大量的容错。关于类型转换和容错以后会单独出一篇文章谈论。

总结

  • YYModel经过扩展实现了无侵入式操做
  • 协议使Model与YYModel进行数据交互
  • YYClassInfo封装Model类型的runtime数据
  • _YYModelPropertyMeta将属性与取值信息绑定
  • _YYModelMeta封装全部的_YYModelPropertyMeta属性
  • 最后经过runtime接口调用属性对应的setter方法赋值
相关文章
相关标签/搜索