内容简介:承接上文首先字典转模型,就是字典中key对应的value赋值给model对应的属性的过程,默认情况下我们都会将属性名对应成字典的key,那么如果我们不想这么起名字。或者我们有这样一个json:
承接上文 《YYModel源码分析(一)YYClassInfo》
之前文章讲述了 YYClassInfo
如何将runtime类结构封装到OC层。这篇文章主要讲述YYModel是如何用 NSObject
分类,实现非侵入式json-model的(类型转换,容错,model转json会在其他文章中讨论)。
写在开头
NSObject+ YYModel
中并不只有 NSObject
分类,还包含了 _YYModelPropertyMeta
和 _YYModelMeta
以及协议 <YYModel>
,当然又声明了很多静态(内联)函数,至于为什么用内联函数而不用类方法或者宏定义,是因为内联函数在编译中会将代码插入到调用的位置,这样会提高调用效率,相对于宏又有函数的特点。具体可以看这里《IOS 内联函数Q&A》。
协议
首先字典转模型,就是字典中key对应的value赋值给model对应的属性的过程,默认情况下我们都会将属性名对应成字典的key,那么如果我们不想这么起名字。或者我们有这样一个json:
{ "n":"Harry Pottery", "p": 256, "ext" : { "desc" : "A book written by J.K.Rowling." }, "ID" : 100010 } 复制代码
我们想赋值给这个model
@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>
协议中的取值信息。
/// 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
当中,将其中有效的信息封装到该类中。
@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方法赋值
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
编程之美:微软技术面试心得
《编程之美》小组 / 电子工业出版社 / 2018-9 / 79
《编程之美:微软技术面试心得》收集了约60道算法和程序设计的题目,这些题目大部分在微软的笔试、面试中出现过,有的曾被微软员工热烈地讨论过。作者试图从书中各种有趣的问题出发,引导读者发现问题、分析问题、解决问题,寻找更优的解法。《编程之美:微软技术面试心得》内容分为以下几个部分。 游戏之乐:从游戏和其他有趣问题出发,化繁为简,分析总结。 数字之魅:编程的过程实际上就是和数字及字符打交道的......一起来看看 《编程之美:微软技术面试心得》 这本书的介绍吧!