发掘YYModel逻辑

本文大约3800字,将会在 Lision 的两篇关于YYModel解析的优秀文章的基础上,讲述一些 Lision 没有提到的逻辑细节。

!!!原创文章,转载请注明来源: pany.fun

首先两篇 Lision 的文章如下,建议有兴趣的朋友先行阅读再回到本文。当然,如果没有用过YYModel,建议先行对 YYModel的基本使用 进行了解。

  • 揭秘YYModel魔法(上)

    主要涉及一些对YYModel的基本介绍,如框架结构、各个文件的介绍、简单代码细节的剖析 以及 YYModel用到的一些技术(主要是runtime)的基本原理 等

  • 揭秘YYModel魔法(下)

    详细介绍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 的关系的。

    1. 一级映射

      形如 (json)name -> (model)name 或是 (json)name->(model)userName 通过一层关系就能取到值的映射(我们姑且称它为一级映射…),这里无关于model中的属性名与json中的key名是否相同,之和映射的层级数有关

    2. 多级映射

      形如 (json)user.name -> (model)name 的映射就是多级映射,它需要在json中通过多层关系才能取到值

    3. 单关系映射

      model中的一个属性,在json中有且只有一个key与之相映射(我们姑且称这种为单关系映射…)

    4. 多关系映射

      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逻辑/