发掘YYModel逻辑
本文大约3800字,将会在 Lision 的两篇关于YYModel解析的优秀文章的基础上,讲述一些 Lision 没有提到的逻辑细节。
!!!原创文章,转载请注明来源: pany.fun
首先两篇 Lision 的文章如下,建议有兴趣的朋友先行阅读再回到本文。当然,如果没有用过YYModel,建议先行对 YYModel的基本使用 进行了解。
-
主要涉及一些对YYModel的基本介绍,如框架结构、各个文件的介绍、简单代码细节的剖析 以及 YYModel用到的一些技术(主要是runtime)的基本原理 等
-
详细介绍YYModel的 model->json 以及 json->model的代码逻辑,但是也有一些因篇幅而不得不忽略的点。
本文讲述的就是自己在阅读上述两篇文章的过程中,对于作者迫于篇幅而舍弃或忽略的逻辑细节的理解(有一些细节真的很头大),作者已经讲述的内容将不再赘述。
为了方便表述,文中将用来给model赋值的json / dic 都描述为了json。但是其实它们是有一定区别的,例如dic可能是客户端生成的,其中的某些value已经是对象类型了,在转换过程中就不需要再进行隐式类型转换。
#2个类
_YYModelMeta
NSObject+YYModel.m -> line:454
用于记录 类信息 以及 类的属性映射分类
// NSObject+YYModel.m -> line:454
// 为了方便理解,删除了部分无关内容
// A class info in object model.
@interface _YYModelMeta : NSObject {
@package
YYClassInfo *_classInfo;
NSDictionary *_mapper; //<NSString *, _YYModelPropertyMeta *>
NSArray *_allPropertyMetas; //<_YYModelPropertyMeta *>
NSArray *_keyPathPropertyMetas; //<_YYModelPropertyMeta *>
NSArray *_multiKeysPropertyMetas; //<_YYModelPropertyMeta *>
NSUInteger _keyMappedCount;
}
NSArray *_allPropertyMetas
容器类型
<_YYModelPropertyMeta *>
,存放该类所有的属性的信息。NSDictionary *_mapper
容器类型
<NSString *, _YYModelPropertyMeta *>
,存放所有的映射关系,并且是以json中的key作为这个mapper的key,方便后面json->model取用。YYModel中映射关系我们大致可以分成几类,分类可以从两种角度(映射的层级数、映射关系的数量)考虑,每种角度下都可以将映射分为两类,我们直接列出来,但是注意 1or2 与 3or4 只是不同的分类角度,它们是可以叠加产生诸如 2&3 的关系的。
一级映射
形如 (json)name -> (model)name 或是 (json)name->(model)userName 通过一层关系就能取到值的映射(我们姑且称它为一级映射…),这里无关于model中的属性名与json中的key名是否相同,之和映射的层级数有关
多级映射
形如 (json)user.name -> (model)name 的映射就是多级映射,它需要在json中通过多层关系才能取到值
单关系映射
model中的一个属性,在json中有且只有一个key与之相映射(我们姑且称这种为单关系映射…)
多关系映射
model中的一个属性,在json中有多个key与之相映射,例如(json)Id/ID -> (model)usrId。这种映射用于一个model在多处使用的时候,可能需要从不同的json转换而来。
NSArray *_keyPathPropertyMetas
容器类型
<_YYModelPropertyMeta *>
,用于存放该类的多级映射属性( 形如 (json)user.name -> (model)name )。源码中有这么一段能帮助我们理解这个数组容器存放的内容
// NSObject+YYModel.m -> line: 551 // 为了方便理解,删除了部分无关内容 // 取用户自行定义的的映射关系字典做以下处理 // 外部传入的参数 // NSString *propertyName, NSString *mappedToKey; _YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName]; if ([mappedToKey isKindOfClass:[NSString class]]) { if (mappedToKey.length == 0) return; propertyMeta->_mappedToKey = mappedToKey; NSArray *keyPath = [mappedToKey componentsSeparatedByString:@"."]; for (NSString *onePath in keyPath) { if (onePath.length == 0) { NSMutableArray *tmp = keyPath.mutableCopy; [tmp removeObject:@""]; keyPath = tmp; break; } } if (keyPath.count > 1) { // 当keyPath是多级映射(count>1)的时候加入到keyPathPropertyMetas中,否则直接加入mapper,并以json中的key作为mapper的key propertyMeta->_mappedToKeyPath = keyPath; [keyPathPropertyMetas addObject:propertyMeta]; } propertyMeta->_next = mapper[mappedToKey] ?: nil; // 暂时不用关注 mapper[mappedToKey] = propertyMeta; ... }
NSArray *_multiKeysPropertyMetas
容器类型
<_YYModelPropertyMeta *>
,用于存放该类的多关系映射属性( 形如 (json)Id/ID -> (model)usrId )。源码中也有一段代码能够帮助我们理解这个数组容器存放的内容
// NSObject+YYModel.m -> line: 576 // 为了方便理解,删除了部分无关内容 // 取用户自行定义的的映射关系字典做以下处理 // 外部传入的参数 // NSString *propertyName, NSString *mappedToKey; if ([mappedToKey isKindOfClass:[NSArray class]]) { NSMutableArray *mappedToKeyArray = [NSMutableArray new]; // mappedToKeyArray中会存放两种格式的东西 // 如果映射是一级映射,则直接存放keyPath字符串 // 如果映射是多级映射,则存放keyPath拆分后数组 for (NSString *oneKey in ((NSArray *)mappedToKey)) { if (![oneKey isKindOfClass:[NSString class]]) continue; if (oneKey.length == 0) continue; NSArray *keyPath = [oneKey componentsSeparatedByString:@"."]; if (keyPath.count > 1) { [mappedToKeyArray addObject:keyPath]; } else { [mappedToKeyArray addObject:oneKey]; } if (!propertyMeta->_mappedToKey) { propertyMeta->_mappedToKey = oneKey; propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil; } } if (!propertyMeta->_mappedToKey) return; propertyMeta->_mappedToKeyArray = mappedToKeyArray; [multiKeysPropertyMetas addObject:propertyMeta]; // 经过校验后,如果该属性的mappedToKey有效,则加入到multiKeysPropertyMetas propertyMeta->_next = mapper[mappedToKey] ?: nil; // 暂时不用关注 mapper[mappedToKey] = propertyMeta; }
NSUInteger _keyMappedCount
用于记录映射的总数量。YYModel的注释和 Lision 都描述为 “等同于 _mapper.count”。源码中发现,他们确实描述的非常精准,真的只是“等同”而已…因为它并不是直接取的mapper的count,而是取的allPropertyMetas的数量(虽然不明白其中道理)。
// NSObject+YYModel.m -> line: 617 _keyMappedCount = _allPropertyMetas.count;
> 你可能会有的疑问
看完这么几个比较重要的属性以后,你或许注意到了这么两行代码
propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;
先看第二行,无论哪种映射条件下,propertyMeta都会被加入总映射表mapper。mapper是字典类型,key是json中的key,value是propertyMeta。
“我们是不是考虑漏了一种情况?如果json中的一个key被多个属性映射了怎么办(比如 (json)name -> (model)sthName, (json)name -> (model)Id)?毕竟mapper是个直接放propertyMeta的字典,多次向字典内添加具有相同key的内容会覆盖的吧?🤔🤔🤔”
这种情况当然也是有的,但是不要慌张,YYModel已经处理好保证不会覆盖啦😉,第一行代码就是YYModel对这种情况的处理:
如果要映射的key在mapper中已经存在了(直接设置会覆盖),YYModel会通过自己构造一个链表的方式,将相同映射key的属性链接起来,propertyMeta->_next指向另一个属性,这样就能够保证每个属性都不掉队啦。
_YYModelPropertyMeta
NSObject+YYModel.m -> line:320
用于记录 属性的信息 以及 属性的映射关系 等
这个类需要注意的点不多,简单了解一下几个东西就行
// NSObject+YYModel.m -> line:320
// 为了方便理解,删除了部分与讲解无关的内容
@interface _YYModelPropertyMeta : NSObject {
@package
...
Class _cls; // 属性所属的类
Class _genericCls; // 属性如果是容器(如NSArray),容器内装的类型
NSString *_mappedToKey;
NSArray *_mappedToKeyPath;
NSArray *_mappedToKeyArray;
...
_YYModelPropertyMeta *_next;
}
Class _genericCls
需要和_cls区分一下,这个参数只有当属性是容器(如NSArray, NSDictionary),则用这个参数记录容器内装的对象所属类,简单来说,就是容器的泛型类。
NSString *_mappedToKey
如果属性与json是单关系的一级映射( 形如 (json)name->(model)userName ),则用此参数记录该属性映射到json中的key
NSArray *_mappedToKeyPath
如果属性与json是单关系的多级映射( 形如 (json)user.name -> (model)name ),则用此参数记录该属性映射到json中的path,以’.’分解后的有效数组
NSArray *_mappedToKeyArray
如果属性与json是多关系映射,则记录到这个数组中,这个数组中既可以放一级映射的NSString,也可以放多级映射以’.’分解后的有效数组
YYModelPropertyMeta *next
当有多个属性映射到同一个json中的key的时候,用这个next将它们以链表的形式连起来
#核心方法
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic
NSObject+YYModel.m -> line:1478
YYModel的核心方法之一,也是json/dic转model的主流程入口之一
这个方法 Lision 的原文中也花了一些篇幅来讲,但是有些细节我们还是可以再拿出来说的更明白的。
// NSObject+YYModel.m -> line: 1497
// 为了方便理解,删除了部分与讲解无关的内容
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
...
ModelSetContext context = {0};
context.modelMeta = (__bridge void *)(modelMeta);
context.model = (__bridge void *)(self);
context.dictionary = (__bridge void *)(dic);
if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
// Operation 1
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
// Operation 2
if (modelMeta->_keyPathPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
// Operation 3
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);
}
}
条件判断
这个方法不长,占比较多的是一个条件判断。
modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)
这是一个略微有点古怪的判断,但是其实作者的目很简单——通过判断找到数量比较少的容器开始遍历赋值,以Function执行的次数,加快模型转换速度。这个条件翻译一下是这样的
当model中的属性,多于或等于dic中的键值对数量时,使用较少的一方,即dic,进行遍历并往model中赋值。大多数情况我们的model的属性数量会与dic中的键值对数量相同或略多。
当model中的属性,少于dic中的键值对数量时,使用较少的一方,即model的属性表,进行遍历并往model中赋值。当json中有无用信息的时候,会出现这种情况。
Operation 1 : 对 dic 遍历执行 ModelSetWithDictionaryFunction 方法
如果只看到这里,按正常理解,如果dic记录了所有的键值对,那么对它进行遍历应当就已经可以完成赋值工作啦,不需要再有多余的操作啦。但是我们却显然看到这一行代码后仍有其它操作。
而事实上,确实Operation 1能够支撑大部分的业务场景,Lision原文也很准确的将它描述为
一般情况下就是靠 ModelSetWithDictionaryFunction 通过字典设置模型
。既然都已经谈到了”一般”,自然我们会有不一般的情况,这些情况归类起来就是我们前面谈到的 ‘多级映射’ 以及 ‘多关系映射’因为dic是json转换来的字典,通过ModelSetWithDictionaryFunction的代码(后面会详谈) 我们也能够看到,它实际上就是一个dic遍历的回调函数,会取出key和value进行后续赋值操作,而dic的key显然一般不会直接存在”user.name”这种情况,所以对dic的遍历只能够完成一级单关系映射。
Operation 2 : 对 _keyPathPropertyMetas 遍历执行 ModelSetWithPropertyMetaArrayFunction 方法
经过对Operation 1 以及对 _keyPathPropertyMetas 的讲解,这一步应该就已经好理解了,它的目的就是完成Operation 1 完成不了的操作之一——对 多级简单映射 进行处理并赋值。具体的ModelSetWithPropertyMetaArrayFunction我们后面再谈。
Operation 3: 对 _multiKeysPropertyMetas 遍历执行 ModelSetWithPropertyMetaArrayFunction 方法
这一操作逻辑同 Operation 2 ,用于完成对 多关系映射 的处理并赋值。
static void ModelSetWithDictionaryFunction(const void key, const void value, void *context)
NSObject+YYModel.m -> line:1115
用于完成对 一级单关系映射 的转换处理,以及对映射到同一个key的属性的转换处理。
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) {
if (propertyMeta->_setter) {
ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
}
propertyMeta = propertyMeta->_next;
};
}
这一段代码相对简单,其中的while循环主要是用来完成对 映射到同一个dic中的key的属性构成的链表进行遍历赋值。其中的赋值方法 ModelSetValueForProperty 会在后面讲述。
static void ModelSetWithPropertyMetaArrayFunction(const void _propertyMeta, void _context)
NSObject+YYModel.m -> line:1115
用于完成对 多级映射 以及 多关系映射 的转换处理。
这个方法是 Lision 没有具体讲到的一个方法,貌似是被误看成了 ModelSetWithDictionaryFunction ,虽然功能上也差不多…
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);
}
}
这一段代码也是相对简单的,主要也就是中间部分的条件判断,但是有了前面对mappedToKeyArray和mappedToKeyPath的解释铺垫也就好理解了。
YYValueForMultiKeys 和 YYValueForKeyPath 都是一个简单的对dic 循环or递归 取值的方法,相对简单不细讲啦,可以参看YYModel源码。
static void ModelSetValueForProperty(id model, id value, YYModelPropertyMeta *meta)
NSObject+YYModel.m -> line:784
用于完成 对属性赋值的工作,也包含一些附加工作,例如 自动类型转换、容器内类型处理
为了让标题不太长,删除了部分信息,完成的方法名参考源码
这个方法非常非常的长,784行 -> 1099行,但是其中大多是一些类型判断以进行对应处理,建议大家还是多花些时间细细看看源码。
这里需要提到的关于自动转换,YYModel还是存在一些不足的
容器内的自动类型转换无法完成。从源码我们能看出,这一块是空白的,没有做相关的处理
当我们的model的某个容器属性 如NSArray里放的是NSDate / NSNumber 时,无法自动类型转换。
虽然我们额外处理一下也能完成需求,但是欠缺总是种遗憾。PS:不要问我为什么会有这种需求…
对于CGSize / CGRect 等基本的系统结构体,无法完成 标准字符串 到 结构体的自动类型转换
这导致当我们的model中存在如CGSize / CGRect 等属性时,赋值有那么一些原始和乏味 (完成需求是没问题的) 。虽然这是因为runtime无法区分结构体的具体类型…
但是个人觉得,还是有办法支持的,例如由业务层返回类型。对于 无统一明确格式规则的字符串 转 CGSize / CGRect 可以不提供自动支持,但是标准格式的字符串还是可以提供支持的,毕竟 CGSize / CGRect 系统也提供了对应的字符串转换方法。针对以上问题,后期计划fork一份YYModel,并自己尝试解决 🌝🌝
其它东西 Lision 的文章里都讲得很好啦~不再赘述
创作不易,转载请注明来源!pany.fun
本文链接:http://pany.fun/post/发掘YYModel逻辑/