YYModel实现原理
其实很早开始就想写这个了,因为自己一直在用这个库,而且它的代码量也比较少,另外作者ibireme写的代码质量很高。为了提高自己,拜读大神写的库是一个很好的方式。
本文将从头开始分析代码,所以文字可能会比较多。
YYClassMethodInfo
这个类保存了 Method 的信息(其实后面好像也没怎么用到。。。)。 该类的属性及描述如下:
属性名字 | 描述 |
---|---|
Method method | 对应的Method |
NSString *name | 方法的名字 |
SEL sel | 方法选择器 |
IMP imp | 方法实现 |
NSString *typeEncoding | 参数及返回值类型的编码 |
NSString *returnTypeEncoding | 返回值类型的编码 |
NSArray<NSString *> *argumentTypeEncodings | 参数类型的编码 |
想更多的了解类型编码可以去看Type Encodings这篇博客,主要是runtime加快消息的分发
你可以使用@encode
将相应类型转换成内部表示的字符串,当然也有一大部分内部使用的类型编码无法用@encode()
返回。
YYClassPropertyInfo
这个类保存了 Property 信息,主要用到等有setter
和getter
方法,成员变量名称,编码方式(很重要,根据这个来确定属性类型以及其它修饰符)。该类的属性及描述如下:
属性名字 | 描述 |
---|---|
objc_property_t property | 对应的Property |
NSString *name | 名字 |
YYEncodingType type | 类型编码的类型 |
NSString *typeEncoding | 类型编码 |
NSString *ivarName | 成员变量名称,加了个_前缀 |
Class cls | 属性类型 |
NSArray<NSString *> *protocols | 被包含着的协议,可能为空 |
SEL getter | getter方法,不能为空 |
SEL setter | setter方法,不能为空 |
属性转换成YYClassPropertyInfo
的过程比较麻烦,比较复杂的地方在于需要通过解析属性的类型编码,来确定 YYClassPropertyInfo 的YYEncodingType type
属性。
YYEncodingType 主要包含了以下几方面的信息:
- YYEncodingTypeMask: 属性值的类型,例如c的基本数据类型,结构体,id类型,class类型
- YYEncodingTypeQualifierMask:不知道怎么形容这个,在Type Encodings这篇博客里面说是内部使用的类型编码。例如:const,in,out
- YYEncodingTypePropertyMask:属性关键词,例如:strong, weak, readonly
YYClassIvarInfo
这个类保存了 Ivar 的信息。貌似不会用到,这里就不展开讲了。
YYClassInfo
每一个类(包括元类)都有一个对应的 YYClassInfo 实例。非元类对应的实例创建之后会被保存在一个静态字典缓存 classCache 中,元类对应的实例保存在另一个静态字典缓存 metaCache 中,以Class -> YYClassInfo
的映射关系保存在缓存中。如果缓存中没有,则重新创建一个。
创建时,首先会通过 runtime 的方法得到父类,是否是元类,元类,名字等基本信息,然后按顺序分别将它所有的的 Method,Property,Ivar 生成对应的 YYClassMethodInfo,YYClassPropertyInfo,YYClassIvarInfo实例,添加到 YYClassInfo 中。创建完成之后开始创建父类的 YYClassInfo 实例,一直到根类。
YYClassInfo 保存了 Class 的绝大部分信息,但是 objc 是一门动态的语言,可以在运行时添加方法,属性等信息,这也意味着 YYClassInfo 里面保存的信息可能不是最新的。所以当你对 Class 做了一些修改之后,你需要先获得该 Class 对应的 YYClassInfo实例,然后手动调用- (void)setNeedUpdate;
来刷新保存在缓存中的 info 信息。
属性名称 | 描述 |
---|---|
Class cls | 对应类 |
Class superCls | 对应类的父类 |
Class metaCls | 对应类的元类 |
BOOL isMeta | 对应类是否是元类 |
NSString *name | 类名 |
YYClassInfo *superClassInfo | 父类对应的 YYClassInfo 实例 |
NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos | 所有成员变量信息 |
NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos | 所有方法信息 |
NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos | 所有属性信息 |
YYModel协议
如果默认的模型转换不能满足你的需求,那么你可以通过实现 YYModel 协议中的方法达到自定义键值转化的过程。下面,简单介绍一下它的几个方法:
- + (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper;
实现这个方法,你可以自定义mode property -> json key
之间的映射关系,你可以定义一个属性映射多个 key。举个注释中的例子:
1 | json: |
在上面的那个例子中,实现该方法后,字典中 n 对应于属性 name,p 对应于属性 page。。。
- + (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
实现该方法,你可以自定义容器类属性的泛型。例如 NSArray 和 NSDictionary 类的属性,你可以通过该方法你定义它们的元素(数组) value(字典)的类型
- + (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;
通过该方法,你可以自定义 json/字典 在不同的情况转化成不同 Class 的实例。
1 | @class YYCircle, YYRectangle, YYLine; |
- + (nullable NSArray<NSString *> *)modelPropertyBlacklist;
黑名单,数组中的元素代表的是属性的名字。如果这个方法实现了,那么数组中的属性则不会被赋值。
- + (nullable NSArray<NSString *> *)modelPropertyWhitelist;
白名单,数组的元素代表的是属性的名字。如果这个方法实现,那么不在这个数组中的属性不会被赋值
- - (NSDictionary *)modelCustomWillTransformFromDictionary:(NSDictionary *)dic;
在数据模型转换之前,对json字典进行修改。如果实现了这个方法,那么将使用修改后的字典进行mode转换
- - (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic;
如果你想自定义数据模型转换,那么你可以实现这个方法。在这个方法里你可以生成 mode 的实例,并根据 dic 对实例的属性赋值,记得创建成功后返回 YES
- - (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic;
fixme 不知道干嘛
_YYModelMeta
这是一个内部类,它主要用来保存类的信息,每一个类都有一个对应的 _YYModelMeta 实例。创建好的实例会被保存在一个静态字典缓存中 cache,以clss -> _YYModelMeta
的映射关系存储。如果缓存中没有该实例,则重新创建一个。看到这你是不是会觉得很像之前提到的YYClassInfo
?简单点说,YYClassInfo 全盘记录了 class 的信息,_YYModelMeta 是对这些信息进行了加工整理。
下面是 _YYModelMeta 的构造函数,代码比较多,可以直接跳到后面看分析:
1 | - (instancetype)initWithClass:(Class)cls { |
- 创建一个 YYClassInfo 的实例 classInfo
- 如果实现了协议
的方法 modelPropertyBlacklist
,则得到黑名单列表 blacklist - 如果实现了协议
的方法 modelPropertyWhitelist
,则得到白名单列表 whitelist - 如果实现了协议
的方法 modelContainerPropertyGenericClass
,则得到容器类的泛型 genericMapper,映射关系为property -> class
- 创建该类以及父类(不包括根类)所有的元属性 _YYModelPropertyMeta
- 遍历第一步中得到的 classInfo 的 propertyInfos 属性
- 如果 blacklist 不为空,并且该属性的名字在里面,则 continue
- 如果 whitelist 不为空,且该属性的名字不在里面,则 continue
- 创建 _YYModelPropertyMeta 实例 meta,将 YYClassPropertyInfo 信息赋值给 meta,并添加到字典 allPropertyMetas 中,映射关系为
property name ->_YYModelPropertyMeta
- 创建字典 mapper,其映射关系为
mappedToKey -> _YYModelPropertyMeta
,mappedToKey 即json字典中的key。- 如果实现了协议
的方法 modelCustomPropertyMapper
,得到字典 customMapper,映射关系property name -> json key
。为了方便我们把 key 叫做 propertyName, value 叫做 mappedToKey - 遍历 customMapper。根据 propertyName 在 allPropertyMetas 中找到对应的 _YYModelPropertyMeta 实例 meta,如果 meta 不为空,则将其移出 allPropertyMetas
- 如果 mappedToKey 是字符串类型,则将其赋值给 meta 的 _mappedToKey 属性。如果 mappedToKey 的格式使用的 keyPath(类似 @”json.key”),则将该 mappedToKey 使用 @”.” 分割,将分割后等数组赋值给 mata 的 _mappedToKeyPath 属性,并且将 meta 添加到 keyPathPropertyMetas 数组。最后以
mappedToKey -> meta
的映射将其添加到字典 mapper 中, - 如果 mappedToKey 是数组类型,则说明一个属性可能映射了多个json字典的key。此时需要遍历 mappedToKey,它的每一个元素为 oneKey,使用@”.”分割,来判断是否使用了keyPath,分割后得到数组 keyPath。如果 keyPath 的 count 大于1,则将数组 keyPath 添加到 mappedToKeyArray 数组,如果不是则将 oneKey 添加到 mappedToKeyArray,meta 的 _mappedToKey 取值于遍历时第一个 oneKey。当遍历结束,将 mappedToKeyArray 赋值给 meta 的 mappedToKeyArray。最后以
mappedToKey -> meta
的映射关系将其添加到字典 mapper,将 meta 添加到数组 multiKeysPropertyMetas 中 - 遍历第五步中的 allPropertyMetas,为了方便我们将字典中的 key 称为 key,value 称为 meta。 将 key 赋值给对应 meta 的 _mappedToKey,并且以
property name -> meta
的映射添加到 mapper - 当一个json字典的key对应着多个属性时,你可以使用 _YYModelPropertyMeta 的 _next来处理 fixme
- 如果实现了协议
- 最后是为 _YYModelMeta 其它一些属性赋值
从这个函数中我们可以看出,_YYModelMeta 处理了好几种情况下的数据模型转换问题
- 当自定义了映射关系,一个属性对应多个 key 时,使用 _multiKeysPropertyMetas 来处理
- 当自定义了映射关系,一个 key 对应多个属性时,使用 _YYModelPropertyMeta 的 _next 来处理
- _mapper 中包含了 key 跟 属性之间的映射关系
- 默认情况下,key 即为属性名,此时使用 _allPropertyMetas 来处理
- 通过一些bool值来表明 Class 实现了 》
的哪几个方法
通过下图的_YYModelPropertyMeta的属性说明,能帮助更好的理解这一点
属性 | 说明 |
---|---|
_classInfo | 对应YYClassInfo实例 |
_mapper | 字典,映射关系:json字典key -> 属性。如果一个key对应多个属性时,_mapper数量会小于属性数量 |
_allPropertyMetas | 所有的_YYModelPropertyMeta实例数组 |
_keyPathPropertyMetas | 使用了keyPath映射的_YYModelPropertyMeta实例数组 |
_multiKeysPropertyMetas | 被多个key映射的_YYModelPropertyMeta实例数组 |
_keyMappedCount | 等同于_mapper的count |
_nsType | 是 Founddation 的什么类,可能不是 |
_hasCustomWillTransformFromDictionary | 是否实现了协议方法modelCustomWillTransformFromDictionary |
_hasCustomTransformFromDictionary | 是否实现了协议方法modelCustomTransformFromDictionary |
_hasCustomTransformToDictionary | 是否实现了协议方法modelCustomTransformToDictionary |
_hasCustomClassFromDictionary | 是否实现了协议方法modelCustomClassForDictionary |
数据模型转换
通过 _YYModelMeta 生成实例的 modelMeta ,我们可以知道json字典跟属性之间的对应关系。所以,接下来要做的就是数据模型之间转换了。
1 | - (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic { |
首先根据 modelMeta 的 _hasCustomClassFromDictionary 来判断是否自定义了mode的类型,如果是,则获得自定的 mode 类型cls,并根据cls得到对应的 _YYModelMeta 实例 modelMeta。根据 modelMeta 的 _hasCustomWillTransformFromDictionary 来判断是否在转换前json字典进行修改,如果修改了则使用修改后的字典来转换mode。随后生成一个结构体 context,用来存储转换时要用到的信息。
比较 modelMeta 的 _keyMappedCount 与 json字典的 count 之间的大小
- _keyMappedCount 大于等于字典的 count,则首先遍历字典,对里面的键值对调用
ModelSetWithDictionaryFunction
方法。如果 modelMeta 的 _keyPathPropertyMetas 和 _multiKeysPropertyMetas 不为空,则对里面的每个元素调用ModelSetWithPropertyMetaArrayFunction
方法 - _keyMappedCount 小于字典的 count,则对 _allPropertyMetas 里面的每个元素调用
ModelSetWithPropertyMetaArrayFunction
方法
(应该是哪个少遍历哪个,减少开销。。。)
首先让我们看一下上面提到的第一个方法ModelSetWithDictionaryFunction
1 | static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) { |
这个方法比较简单,根据 key 在 meta 的 _mapper 中找到相应的 _YYModelPropertyMeta 实例,随后调用ModelSetValueForProperty
方法为该属性赋值,这个方法稍后再提。因为存在一个 key 对应多个 属性的情况,所以对该属性赋值后,会沿着 _next 继续为其它对应的属性赋值。
然后再看一下第二个方法ModelSetWithPropertyMetaArrayFunction
1 | static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) { |
这个方法里主要做了这么几件事情:
- 找到json字典中的数据
- 调用
ModelSetValueForProperty
方法将数据赋值给属性,是的,又是这个方法
如何取值
当调用 ModelSetWithPropertyMetaArrayFunction 方法时,传入了上下文 context(里面包含了我们要用到的json字典context->dictionary
)和元属性_YYModelPropertyMeta *propertyMeta
。
接下来就是分不同的情况来取值了:
- 如果有多个key对应同一个属性时,那么会取第一个key且相应json字典中value不为空时的value。例如,有一个json @{@”name” : @”11”, @”title” : @”222”}, 如果有 @”age”, @”name”, @”title” 这几个 key 对应同一个属性 name,那么只会取json中 @”name” 对应的值
- 如果使用了 keyPath 来定义属性的映射,那么在json字典中会逐级获取数据(不知道怎么表达了。。。),例如有一个json @{@”info” : @{@”name” : @”111”}}, 并且使用@”info.name”来映射属性,那么首先会取得 @”info”对应的字典 dic,然后再在dic中取得@”name”的值
- 如果没有上述两种情况,则直接根据key在json字典中取值
好了,是不是看了感觉还挺简单的,复杂的其实在赋值这一步!
如何赋值
赋值的过程比较复杂,且代码量比较多, 这里就不贴出来了。在这里我简单的分析一下过程:
首先属性是基本数据类型
1 | if (meta->_isCNumber) { |
这个比较简单。首先将从json字典得到的value进行处理,得到一个 NSNumber 类型的数据 num。然后将 num 转换成相应类型的数据,通过objc_msgSend
消息发送赋值给该属性。由于在赋值的函数中参数的类型是__unsafe_unretained
(类似weak),所以需要在赋值成功前持有该数据,否则程序会因为 num 成为野指针而崩溃,所以在ModelSetNumberToProperty
后面还有这样一行代码if (num) [num class];
,看似没用,其实还是有点用的。如果你想对__unsafe_unretained
了解深一点可以看孙源的这篇博客
然后时属性属于 Foundation 类型时,会先将 value 转换成属性的类型meta->_nsType
,然后通过objc_msgSend
赋值给属性。
当属性属于数组(NSArray, NSMutableArray)和字典(NSDictionary, NSMutableDictionary)时复杂一点:
- 数组
- 如果没有指定泛型,那么直接把value复制给属性
- 会遍历数组value,将元素转换成相应的泛型,然后添加到一个新数组value中,最后将该value赋值给属性
- 字典
- 如果没有指定泛型,那么直接把value复制给属性
- 会遍历字典values,将value转化成相应的泛型,添加到一个新字典vlues中。最后将该values赋值给属性
属性属于其它的类型,例如自定义的 objc 类,block,c的结构体,联合,数组等,转换过程跟之前也是差不多的。
总结
写了好几天,终于完成了。希望大家看完后对这个库的使用能够有所帮助。在这里可算是帮我解决了个疑问:
在自定义mode中就算为容器类指定了泛型,但转换的时候还是会失败, 原因是我们不能在类型编码中得到泛型的信息…
引用:YYModel