七、Runtime 实践部分之 YYModel 源码跟读(二)NSObject+YYModel

本文github地址 https://github.com/ICZhuang/Runtime 朋友们如果喜欢收藏即可,不建议转载,笔者会不定期update文章,github 也会同步更新,喜欢就star一下吧。
在YYKit中,模型字典的互转,以NSObject的分类来实现,这个分类就是NSObject+YYModel。

作为分类方法,任何继承自NSObject的 类都可以直接调用 +modelWithDictionary: 并将一个字典作为参数,快速创建一个包含与字典中匹配键值的属性的实例对象。

+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary {
    if (!dictionary || dictionary == (id)kCFNull) return nil;
    if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;
    /// 1.creat model meta
    Class cls = [self class];
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
    if (modelMeta->_hasCustomClassFromDictionary) {
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }
    /// 2.set value
    NSObject *one = [cls new];
    if ([one moelSetWithDictionary:dictionary]) return one;
    return nil;
}

这个方法分两个部分,一是创建一个_YYModelMeta实例对象,二是创建一个本类实例对象并根据字典对应赋值。


_YYModelMeta


这是内部的一个私有类,顾名思义,这个类就是用来封装,更贴切的说是记录(或者引用)模型类中的一些数据的,与模型类是一一对应的。

目的就是为了后面对模型更快跟准确赋值用的。所以有一个缓存专门存储这个模型类的_YYModelMeta。(就称需要用到模型字段互转的类为模型类,后面也用这个称呼)。

开始笔者认为 _YYModelMeta 并不准确,meta 是指元数据,就是这个类是存字段那些信息的,所以笔者在纠结 _YYModelMeta 和 _YYClassMeta 哪个名字更准确,后来是笔者想岔了,这个类不就是存储model的meta数据么,class meta 岂不是类的元数据?类的元数据应该在元类(meta class)吧.. 况且 _YYModelMeta 类同时会遍历并存储其父类的属性信息。。

@interface _YYModelMeta : NSObject {
    /// 该类的ClassInfo
    YYClassInfo *_classInfo;
    /// 用来索引
    /// 字典的键默认用属性名作为键, 否则如果用 +modelCustomPropertyMapper 映射的,则用映射后的做为键
    /// 字典的值是对应的_YYModelPropertyMeta实例对象
    NSDictionary *_mapper;
    /// 用来存储所有的_YYModelPropertyMeta实例对象
    NSArray *_allPropertyMetas;
    /// 用来存储需要用keyPath(xxx.xxx.xxx)索引的_YYModelPropertyMeta实例对象
    NSArray *_keyPathPropertyMetas;
    /// 用来存储有多个key索引的_YYModelPropertyMeta实例对象
    NSArray *_multiKeysPropertyMetas;
    /// 所有编入索引的_YYModelPropertyMeta实例对象的数量 _allPropertyMetas.count
    NSUInteger _keyMappedCount;
    /// 该类对象的NS类型,辨别该类是不是属于Foundation框架的子类
    YYEncodingNSType _nsType;
    /// 是否在转换之前对原dic做了中间处理
    BOOL _hasCustomWillTransformFromDictionary;
    /// 是否有定义要不要返回(或处理)dic转换之后的对象
    BOOL _hasCustomTransformFromDictionary;
    /// model转json中,是否有定义要不要处理转换后的对象
    BOOL _hasCustomTransformToDictionary;
    /// 是否有定义实际对象的类型,参考协议方法 +modelCustomClassForDictionary:
    /// 根据dictionary出现的不同字段值,判定究竟应该返回什么类型的对象
    BOOL _hasCustomClassFromDictionary;
}

_YYModelPropertyMeta 顾名思义,也可以知道它封装了property 的元数据,后续将介绍它的数据结构。可见YYModel 是核心是对属性的遍历和操作,并不涉及对 Ivar (成员变量)。

_YYModelMeta 通过类方法 +initWithClass: 创建对象。它里面是在对模型对象的YYClassInfo做工作。


1、白名单(white list)/ 黑名单(black list)

应该更准确的称呼它们,那就是属性白名单和属性黑名单,这名单是用来区分在转换过程中哪些属性是需要处理的和哪些是需要忽略的,那就是白则需要处理,黑则不需要处理。

// Get black list
NSSet *blacklist = nil;
if ([cls respondsToSelector:@selector(modelPropertyBlacklist)]) {
    NSArray *properties = [(id<YYModel>)cls modelPropertyBlacklist];
    if (properties) {
        blacklist = [NSSet setWithArray:properties];
    }
}
// Get white list
NSSet *whitelist = nil;
if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) {
    NSArray *properties = [(id<YYModel>)cls modelPropertyWhitelist];
    if (properties) {
        whitelist = [NSSet setWithArray:properties];
    }
}

可以通过实现 Protocol YYModel 的两个方法来提供这两个名单。(如果名单有交集,可以试试哪个优先)

+ (nullable NSArray<NSString *> *)modelPropertyBlacklist;
+ (nullable NSArray<NSString *> *)modelPropertyWhitelist;

NSObject+YYModel.h中对方法都做了注释,虽说是英文,但都易懂。应该看看注释。

2、Container Property's Generic Class

同样根据 Protocol YYModel 提供的协议方法来定义容器类型属性中元素类型,捋顺了讲,就是说如果属性是容器类型,通过这个方法来映射该属性应该装什么类型的对象。使用方法参考它的注释。

+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;

协议方法的返回值是 NSDictionary<NSString *, id> *,但是内部会进行过滤不合格的 id。得出来的结果就是一个{key<property name> : value<class>} 形式的 NSDictionary。

NSDictionary *genericMapper = nil;
if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
    genericMapper = [(id<YYModel>)cls modelContainerPropertyGenericClass];
    if (genericMapper) {
        NSMutableDictionary *tmp = [NSMutableDictionary new];
        [genericMapper enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            if (![key isKindOfClass:[NSString class]]) return;
            Class meta = object_getClass(obj);
            if (!meta) return;
            if (class_isMetaClass(meta)) { /// Means that 'obj' is the 'Class' type
                tmp[key] = obj; /// The value is still 'obj'
            } else if ([obj isKindOfClass:[NSString class]]) {
                Class cls = NSClassFromString(obj); /// Get 'Class' if 'obj' is a NSString
                if (cls) {
                    tmp[key] = cls;
                }
            }
        }];
        genericMapper = tmp;
    }
}

在声明容器属性的时候,加上泛型并不能起到规范类型效果。它只是在使用该属性时比较方便。

3、Property Metas

循环向上追溯遍历类与父类(除了NSObject/NSProxy)的各个属性,利用类中的property info 对象创建_YYModelPropertyMeta,如果不存在setter或者getter方法,则跳过。如果子父类有相通名字的属性,采用子类的_YYModelPropertyMeta。

NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
YYClassInfo *curClassInfo = classInfo;
// recursive parse super class, but ignore root class (NSObject/NSProxy)
while (curClassInfo && curClassInfo.superCls != nil) {   
    for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) {
        if (!propertyInfo.name) continue;
      
        if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
        if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
      
        _YYModelPropertyMeta *meta = [_YYModelPropertyMeta 
                                      metaWithClassInfo:classInfo
                                      propertyInfo:propertyInfo
                                      generic:genericMapper[propertyInfo.name]];
      
        if (!meta || !meta->_name) continue;
        if (!meta->_getter || !meta->_setter) continue;
        if (allPropertyMetas[meta->_name]) continue;
        allPropertyMetas[meta->_name] = meta;
    }
    curClassInfo = curClassInfo.superClassInfo;
}
if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy;

4、Mapper

根据对 _YYModelMeta 的 _mapper 字段描述,先需检查是否有通过 +modelCustomPropertyMapper 协议方法来返回属性对应在 NSDictionary 的键,如果有,则先要把这些 _YYModelPropertyMeta 加入 mapper 中。

这里可能要结合 _YYModelPropertyMeta 一节看,因为涉及到了它里面的字段设置了

NSMutableDictionary *mapper = [NSMutableDictionary new];
NSMutableArray *keyPathPropertyMetas = [NSMutableArray new];
NSMutableArray *multiKeysPropertyMetas = [NSMutableArray new];

if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
    NSDictionary *customMapper = [(id <YYModel>)cls modelCustomPropertyMapper];
    [customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName,
                                                      NSString *mappedToKey,
                                                      BOOL *stop) {
        _YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName];
        if (!propertyMeta) return;
      
      /// 如果属性有被自定义映射,则将它从allPropertyMetas移除
      /// 至于剩余的那些会在后面循坏形式统一加到mapper中
        [allPropertyMetas removeObjectForKey:propertyName];
      
        if ([mappedToKey isKindOfClass:[NSString class]]) {
            if (mappedToKey.length == 0) return;
            /// 设置它的_mappedToKey
            propertyMeta->_mappedToKey = mappedToKey; 
            /// 看它的 mapper key是不是keyPath(xxx.xxx.xxx)
            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,则把property meta 加进keyPathPropertyMetas中
                propertyMeta->_mappedToKeyPath = keyPath;
                [keyPathPropertyMetas addObject:propertyMeta];
            }
           /// 众所周知,NSDictionary 不可能存在同样的 key,如果多个属性同时映射一个 key
           /// 这个时候就将前面的那个 meta 作为后面的 met a的 next,就如同链表一样。
            propertyMeta->_next = mapper[mappedToKey] ?: nil;
           /// 将 property meta 存进mapper中
            mapper[mappedToKey] = propertyMeta;

        } else if ([mappedToKey isKindOfClass:[NSArray class]]) {
            /// mappedToKey 也可能是数组(参考 +modelCustomPropertyMapper 用法)
            /// 代表多个一个属性映射多个 key
            NSMutableArray *mappedToKeyArray = [NSMutableArray new];
            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) {
                    /// 如果是keyPath,将分割后的数组存进mappedToKeyArray
                    [mappedToKeyArray addObject:keyPath];
                } else {
                    /// 如果只是key,则直接把key存进mappedToKeyArray
                    [mappedToKeyArray addObject:oneKey];
                }
                /// 存储它的 key 和 keyPath
                if (!propertyMeta->_mappedToKey) {
                    propertyMeta->_mappedToKey = oneKey;
                    propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil;
                }
            }
            if (!propertyMeta->_mappedToKey) return;
            /// 设置 property meta 的mappedToKeyArray
            propertyMeta->_mappedToKeyArray = mappedToKeyArray;
            [multiKeysPropertyMetas addObject:propertyMeta];
            
            propertyMeta->_next = mapper[mappedToKey] ?: nil;
            /// 将 property meta 存进mapper中
            mapper[mappedToKey] = propertyMeta;
        }
    }];
}
/// 遍历那些没有经过自定义映射的,设置它们的字段,并加入mapper中
[allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, 
                                                      _YYModelPropertyMeta *propertyMeta, 
                                                      BOOL *stop) {
    propertyMeta->_mappedToKey = name;
    propertyMeta->_next = mapper[name] ?: nil;
    mapper[name] = propertyMeta;
}];

5、_hasCustomClassFromDictionary

Protocol YYModel中 +modelCustomClassForDictionary: 设计巧妙,可以由开发者根据开发者通过dictionary出现的不同字段值,判定字典转模型的结果应该返回的是什么类型的对象。通常出现在父子类关系中。具体参见该方法注释。

_hasCustomClassFromDictionary = 
([cls respondsToSelector:@selector(modelCustomClassForDictionary:)]);

_YYModelPropertyMeta


这也是内部一个私有类,封装的一个属性的 meta 数据。与属性也是一一对应的。

@interface _YYModelPropertyMeta : NSObject {
    /// 属性名
    NSString *_name;    
    /// 属性类型
    YYEncodingType _type;   
    /// 属性类型(Foundation框架中的类型)
    YYEncodingNSType _nsType;   
    /// 是否为C基本数据类型
    BOOL _isCNumber;             
    /// 属性自己的Class
    Class _cls;                  
    /// 如果该属性是容器类型,_genericCls 则是元素的类型
    Class _genericCls;           
    /// getter 方法
    SEL _getter;  
    /// setter 方法
    SEL _setter;  
    /// 是否支持KVC访问
    BOOL _isKVCCompatible;      
    /// 如果是 struct 类型,是否支持解档归档
    BOOL _isStructAvailableForKeyedArchiver; 
    /// 是否有定义实际对象的类型
    BOOL _hasCustomClassFromDictionary; 
    /// +modelCustomPropertyMapper 映射的key
    NSString *_mappedToKey;      
    /// 如果 +modelCustomPropertyMapper 映射的是keyPath(xx.xx),以‘.’分割后存储
    NSArray *_mappedToKeyPath;   
    /// 如果 +modelCustomPropertyMapper 映射的是多个key或keyPath,都存储在_mappedToKeyArray中,
    /// 这个时候_mappedToKey存储的是第一个元素。
    /// 元素可能同时又 NSString 和 NSArray
    NSArray *_mappedToKeyArray; 
    /// 对应的 property info
    YYClassPropertyInfo *_info;  
    /// 如果多个属性映射同一个Key, 将以链表的形式将它们以next的形式链接起来
    _YYModelPropertyMeta *_next; 
}

_YYModelPropertyMeta 通过类方法创建,其中 generic 则是容器属性中元素的类型

+ (instancetype)metaWithClassInfo:propertyInfo:generic:

1、 _mappedToKey/ _mappedToKeyPath/ _mappedToKeyArray

默认字典中的key与类中属性名一一对应,就是说 _mappedToKey 默认是属性名。 但如果字典中的key找不到与之对应的属性名,那么可以通过实现+modelCustomPropertyMapper方法提供属性与字典key之间的映射,让属性能都找到真正的值。

该方法返回的是NSDictionary<NSString *, id> *,这里 id 可以是 NSString 和 NSArray 类型,其它的类型会没过滤掉。但就这两种类型,组合也可以是多种的。代码参考 _YYModelMeta > mapper creation

  • 如果是key (xxx) 形式的 NSString,那么 _mappedToKey 就是 id, 这时其它两者皆为nil;
  • 如果是keyPath (xxx.xxx.xxx) 形式的NSString,那么 _mappedToKey 值是 id, _mappedToKeyPath 则是 ‘.’ 分割后的数组, _mappedToKeyArray 为nil;
  • 如果是数组(A),那么 _mappedToKey 是数组的第一个元素, 如果第一个元素是第二种形式的NSString,那么 _mappedToKeyPath 为其通过 ‘.’ 分割后的结果, _mappedToKeyArray 则比较复杂,它的元素可能是NSString 或者是NSArray,遍历数组(A),如果是keyPath形式的, 那么存进去的就是分割后的数组,如果是key 形式的直接存储。

2、_hasCustomClassFromDictionary

它的判定方法如下所示。如果存在generic,说明该属性是容器类型,这个字段值应该由元素的类对象判定;如果没有generic则不是容器类型,并且该属性如果是自定义类型,那么根据自身类对象判定。

if (generic) {
    meta->_hasCustomClassFromDictionary = 
    [generic respondsToSelector:@selector(modelCustomClassForDictionary:)];
} else if (meta->_cls && meta->_nsType == YYEncodingTypeNSUnknown) {
    meta->_hasCustomClassFromDictionary = 
    [meta->_cls respondsToSelector:@selector(modelCustomClassForDictionary:)];
}

结语


承文章开头,本文介绍的是字典转模型步骤中的第一部分,就是创建 _YYModelMeta,后半部分就是拿着这些元数据和NSDictionary去分别给属性赋值。NSObject+YYModel通篇几近两千行代码,要都往博客上粘,任重道远呐,笔者还是不干这么愚蠢的事儿了。关于后半部分,笔者想往后有时间再继续往深了读吧。

况且,笔者初衷是想对前面 Runtime 的文章做一些实践的东西,就想到拿YYModel源码做个示例。后半部分还涉及到很多东西,比如通过字符串获得时间,各类时间格式YY大神都写的很全面,笔者不认为不去查阅相关资料就可以有很好的认识。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容