YYModel是由ibireme开发的一套小而精美的模型转换框架,采用分类的形式,无需继承框架的某个基类就可以方便地完成模型的转换,且内部做了自动类型转换和安全处理,可以有效地防止因模型类型和后台给的数据类型不一样而产生的崩溃问题。
近些天抽空拜读了一下其源码,果然是思维严谨,考虑的一些细节也很到位,让人自叹弗如。虽然作者说这个框架是花了两个周末的时间完成的,但是其代码质量还是非常让人惊艳的,值得仔细阅读。
基本结构
YYModel总共由5个文件组成,其中核心文件只有以下四个:
1 | YYClassInfo.h |
YYClassInfo
主要将成员变量、方法、成员属性以及类这四类信息,从C层面的函数调用抽象成OC的类。这个文件主要的类有以下四个:
1 | YYClassInfo // 类 |
YYClassInfo
包含三个以后三者的name
为键,以后三者为值的字典。由于YYModel使用遍历属性的方式来达到模型转换的目的,所以其中的propertyInfos
起比较重要的作用。
该文件还包含一个枚举类型:YYEncodingType
,列举了各类编码信息,包括值类型、方法限定类型、属性修饰类型。YYEncodingType
使用掩码的方式对这三类不同的枚举信息进行分类,各占据1个字节:
1 | YYEncodingTypeMask = 0xFF //值类型 |
其中YYEncodingTypeQualifierMask
和YYEncodingTypePropertyMask
可以进行逻辑与操作,来表示被多个修饰符修饰或限定的属性或方法。
详细的编码信息可以从以下链接获取:
Declared Properties
Type Encodings
核心代码
模型转换的核心功能无非两种:model->json、json->model,所以这里主要纪录下框架中这两个主要功能的阅读笔记。
json->model
yy_modelWithJSON
是json转模型的入口,可以先从这个方法入手。
1 | + (instancetype)yy_modelWithJSON:(id)json { |
以上代码通过_yy_dictionaryWithJSON
将json转换成了字典,再调用yy_modelWithDictionary
对字典进行转换,事实上,真正的对外转换方法应该是yy_modelWithDictionary
,所有对集合内部模型的转换,最终都使用了这个方法。
这里有一个小技巧,就是对方法的命名,对内方法/函数添加’_’前缀,对外去处此前缀,这种做法在CoreFoundation里面也比较常见。
1 | + (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary { |
获取字典之后,就可以生成调用实例所属类信息了。这里关于类_YYModelMeta
作者注释的已经非常详细了:
1 | @interface _YYModelMeta : NSObject { |
_mapper
为所有映射信息,以key为键(最原始的),以_YYModelPropertyMeta
实例为值;要注意的是这里的key还没有被拆分成keyPath数组,还是原始的形式:@”key1.key2”,而且在属性映射到一个数组时,这里的key便是一个数组_allPropertyMetas
为当前模型所有属性的信息_keyPathPropertyMetas
为映射到keyPath属性的信息_multiKeysPropertyMetas
为映射到一个数组的属性信息_YYModelMeta
只存储到属性_YYModelPropertyMeta
层面的信息,更加细化的信息,比如拆分成数组的keyPath,属性映射到的数组等信息,都是交由_YYModelPropertyMeta
进行存储,_YYModelMeta
存储的只是最原始的key信息。
关于_YYModelPropertyMeta
:
1 | @interface _YYModelPropertyMeta : NSObject { |
_mappedToKey
映射到的key(@{property : key})_mappedToKeyPath
映射到的keyPath(@{property : key1.key2})_mappedToKeyArray
映射到的数组(@{property : @[key1, key2]})
每个_YYModelPropertyMeta
中,这三者只有其中一个会有值。有了这三个属性,就可以获取需要转化的对应字典的value了。
这里记录下_YYModelMeta
的init方法执行流程:
1 | 1、根据Class获取YYClassInfo实例 |
一些基本的信息类设置完成后,就开始设置实例的属性了:
1 | // 设置前对传入的字典进行更改 |
当modelMeta->_keyMappedCount
大于等于CFDictionaryGetCount((CFDictionaryRef)dic)
时,
执行以下步骤:
1 | 1、遍历字典,并以字典为基准,设置模型中与字典相对应的属性 |
否则直接通过_allPropertyMetas
设置所有属性。
字典回调函数的如下:
1 | static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) { |
在遍历字典时,回调函数会根据字典的key从_mapper
中获取对应的_YYModelPropertyMeta
,
然后通过ModelSetValueForProperty
设置属性值。如果propertyMeta
的_next
不为空,即表示有多个属性被映射到了同一个key。
这样做的好处是只需要从字典中取一次value,就可以设置被映射到同一个key的所有属性;而通过_allPropertyMetas
设置时,则需要对每个属性
都对字典做一次取值操作。作者在优化Tip的第8点中也提到:
1 | 8. 减少遍历的循环次数 |
数组的回调函数如下:
1 | static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) { |
其中YYValueForMultiKeys
代码如下:
1 | static force_inline id YYValueForMultiKeys(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *multiKeys) { |
在一个属性被映射到多个key时,只取第一个匹配成功的key,后续的key将会被略过。YYValueForKeyPath
的代码如下:
1 | static force_inline id YYValueForKeyPath(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *keyPaths) { |
作者为了优化代码性能,将映射的keyPath
以.
为分隔符拆分成多个字符串,并以数组的形式存储,最终用循环获取value
的方式代替valueForKeyPath:
,也解决了从非字典取value
时的崩溃问题。当然在不考虑性能的情况下,也可以用以下方式实现:
1 | NSString *keyPath = [keyPaths componentsJoinedByString:@"."]; |
YYModel采用objc_msgSend
直接调用Getter/Setter
,替代了使用KVC对属性进行取值/设置。作者的优化Tip3就说明了避免 KVC:
1 | 3. 避免 KVC |
设置属性值通过方法ModelSetValueForProperty
实现。首先,函数把属性分为了三类类型进行处理:
1 | C数值类型 |
其中C数值类型
判断如下:
1 | static force_inline BOOL YYEncodingTypeIsCNumber(YYEncodingType type) { |
NS系统自带类类型
判断如下:
1 | static force_inline YYEncodingNSType YYClassGetNSType(Class cls) { |
需要注意的是mutable
类型一般都继承于immutable
类型,所以需要先判断是否为mutable
。由于类簇的原因,我们是无法在runtime
时获取属性是否是mutable
的,所以只能进行静态判断,这也是_YYModelPropertyMeta
的_nsType
存在的意义。(PS: 在函数ModelSetValueForProperty
中,关于类簇的问题似乎还没有从YYModel主线合入到YYKit的YYModel中。YYKit的YYModel版本,使用了isKindOfClass
来分区分NSMutableString
和NSString
,导致属性类型是NSMutableString
的情况下,获得的还是immutable
版本)
更多关于类簇的资料可以参考:从NSArray看类簇、ClassClusters。
当属性为集合类型时,赋值稍微要麻烦些。比如针对YYEncodingTypeNSArray
,有如下处理代码:
1 | if (meta->_genericCls) { |
在使用者没有通过modelContainerPropertyGenericClass
或者和类型同名的protocols
指定集合中元素的类型时,_genericCls
是为nil的,所以如果value
是NSArray或者NSSet类型,那么YYModel将value
直接赋给属性,并没有做多余的解析。
在使用者已经指定了集合中元素类型的情况下,第一个分支就会对每个元素进行解析并构造成相应的实例。如果集合元素依然是一个字典,那么就会调用yy_modelSetWithDictionary
嵌套解析。
YYModel给使用者提供了极强的扩展性。在解析的过程中,使用者可以根据在modelCustomClassForDictionary:
方法中传入的字典,决定想要生成实例的类型。
model->json
model->json的入口方法为yy_modelToJSONObject
:
1 | - (id)yy_modelToJSONObject { |
主要是利用ModelToJSONObjectRecursive
对model进行嵌套包装,最终生成只包含NSArray/NSDictionary/NSString/NSNumber/NSNull
的JSON对象。ModelToJSONObjectRecursive
的执行逻辑如下:
1 | 1、如果是基本类型:kCFNull、NSString、NSNumber或者nil,直接返回 |
第五步遍历_mapper的代码主要由两部分组成:1、获取属性值;2、构造字典
获取属性值的代码如下:
1 | if (!propertyMeta->_getter) return; |
总体来说和ModelSetValueForProperty
的处理较为类似,也是分的三种大类。
构造字典的代码如下:
1 | // 这里是根据map构造字典 |
小结
很粗略地记录下部分阅读过程。YYModel有不少值得学习的地方,不管是代码风格还是考虑问题的全面性,这些都需要通过阅读源码来了解。