YYModel细读二(易读)

其实YYModel的原理并不多,但是性能和设计上面花了不少时间,而我则希望可以非常快速的理解透彻全码,这也是自己做记录的目的。说不准什么时候又忘了。

回顾一下上篇内容

先来总结一下,YYModel细读一其实就是对YYClassInfo分析,主要是对类的属性、实例、方法的封装,容易糊涂的点在YYEncodingType上。

  1. const一类的限定符
  2. int、NSArray、struct等数据类型
  3. 还有retain,nonatomic,weak等属性特征编码

这篇主要的内容如下:

用到一点runtime和kvc的东西,其实没有太多原理可以讲。赋值读取的方法就是objc_msgSend调用对应选择器SEL来执行(结构体除外,结构体用的是kvc的方式来读写),这样性能比较高。其实可以从我们常用的方法开始读。
我们用到YYModel最常用的方法可能是yy_modelWithJSON,只要这样一个方法,就可以把字典转换为我们需要的模型。
yy_modelWithJSON里面有个yy_modelWithDictionary,yy_modelWithDictionary里面有个metaWithClass(其实这个类只是做了性能优化,后面会讲),metaWithClass里面有个initWithClass。

然后开始细读

从initWithClass开始读,blacklist,whitelist单独搜索这两个就知道,根据这两个表判断要不要加入到allPropertyMetas属性列表中。
下面一段代码,其实很多东西是不用看的了,主要是根据modelContainerPropertyGenericClass生成genericMapper字典,而genericMapper字典,是类名对应一个class(并且这个类不是元类),下面一段就是做这样的处理。而那为什么需要modelContainerPropertyGenericClass这个东西呢?这是因为如果你的属性是NSArray这些对象的时候,就需要自定义modelContainerPropertyGenericClass了。具体实现先不看。

    // Get container property's generic class
    NSDictionary *genericMapper = nil;
    if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
        genericMapper = [(id)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)) {
                    tmp[key] = obj;
                } else if ([obj isKindOfClass:[NSString class]]) {
                    Class cls = NSClassFromString(obj);
                    if (cls) {
                        tmp[key] = cls;
                    }
                }
            }];
            genericMapper = tmp;
        }
    }

搜索genericMapper,可以看到下面代码用到了genericMapper,其实就是传了一个class过去。

_YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
                                                                    propertyInfo:propertyInfo
                                                                         generic:genericMapper[propertyInfo.name]];

这时候发现不怎么好理解,就先不看了。往下走。回到initWithClass方法继续看。
然后是创建allPropertyMetas字典,key为属性名,value是_YYModelPropertyMeta。遍历父类,存储到allPropertyMetas。
_allPropertyMetas存储的是allPropertyMetas的allValues数组。

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

这三个也写了一串的代码,只搜索mapper的话,会发现mapper它的key是属性名或者是mappedToKey。这时最好先看看modelCustomPropertyMapper这个东西,因为这个东西涉及的属性很多。看下面这张图。

YYModel细读二(易读)_第1张图片
image.png

根据上图和modelCustomPropertyMapper的一段代码, mapper则是这样一个情况,@{@"n":propertyMeta,@[@"id",@"ID",@"book_id"]:propertyMeta,@"ext.desc":propertyMeta}
propertyMeta是有包括属性名,还有modelCustomPropertyMapper对应的所有信息的,所以不怕找不到。
keyPathPropertyMetas是针对 @"ext.desc"这种情况的propertyMeta存储的。
其中propertyMeta->_mappedToKeyPath的值都是数组,里面是字符串。
multiKeysPropertyMetas是针对 @[@"id",@"ID",@"book_id"]这种情况的propertyMeta存储的。
其中propertyMeta->_mappedToKeyArray的值是数组或者字符串,.语法都被转成数组了。

    [allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
        propertyMeta->_mappedToKey = name;
        propertyMeta->_next = mapper[name] ?: nil;
        mapper[name] = propertyMeta;
    }];

可以看出属性最后_mappedToKey都是属性名,mapper的key为属性名的都会加上。_next是记录多个属性名对应同一个json的key值的属性。(需要注意的是allPropertyMetas并不包括那些被加入到mapper的属性)
简单想一下就能搞明白,比如属性名有"id","ID",但是json的key就只有"id",假设modelCustomPropertyMapper为

@{
    @"ID":@"id"
}

那么执行顺序是这样的,modelCustomPropertyMapper中有@"ID",从allPropertyMetas中删除@"ID"

   propertyMeta->_next = mapper[mappedToKey] ?: nil;
   mapper[mappedToKey] = propertyMeta;

mappedToKey其实是自定义customMapper(即modelCustomPropertyMapper)的value值,所以情况就是第一行 mapper[@"id"] = nil,第二行mapper[@"id"] = propertyMeta。
allPropertyMetas中并不会出现"ID",因为前面在处理modelCustomPropertyMapper的时候就已经从allPropertyMetas中删除,加入到mapper里。
遍历allPropertyMetas:

    [allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
        propertyMeta->_mappedToKey = name;
        propertyMeta->_next = mapper[name] ?: nil;
        mapper[name] = propertyMeta;
    }];

那么遍历allPropertyMetas(只剩下"id")的时候,第一个_next的就是"id"对应的属性,所以propertyMeta->_next = 其实就是属性@"ID"的propertyMeta ,mapper["id"] = "id"的propertyMeta。其实这样就把ID的propertyMeta挂到id的propertyMeta的_next属性上了。当然也可以从怎么把多个属性对应同一个json的key来想。这个还是需要一些算法思维的。

metaWithClass的作用

metaWithClass的作用就是性能优化,读取缓存。

回到yy_modelWithDictionary

我们是这样一个阅读流程:
yy_modelWithJSON里面有个yy_modelWithDictionary,yy_modelWithDictionary里面有个metaWithClass(其实这个类只是做了性能优化,后面会讲),metaWithClass里面有个initWithClass。
其中yy_modelWithDictionary里面还有几行没有读。
其实一开始就没有讲类与类之间到底是什么关系,其实可以这么认为_YYModelMeta就是比YYClassInfo多了一些映射的数组,是否实现了某些方法的判断,为什么需要在这里加?其实说白了还是性能问题,因为_YYModelMeta是可以拿缓存的,不用每次都调用。只需判断缓存_YYModelMeta的对应那个标记就可以知道是否实现了某个方法,而不是每次调用之前才去判断,这样可以减少多次判断。

    if (modelMeta->_hasCustomClassFromDictionary) {
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }

_hasCustomClassFromDictionary就是这样一个判断,下面一段源码很好的解释了modelCustomClassForDictionary的作用,其实就是根据字典去实例不同的类类型,不用细看了解就好,因为基本上用不着。

 Example:
        @class YYCircle, YYRectangle, YYLine;
 
        @implementation YYShape

        + (Class)modelCustomClassForDictionary:(NSDictionary*)dictionary {
            if (dictionary[@"radius"] != nil) {
                return [YYCircle class];
            } else if (dictionary[@"width"] != nil) {
                return [YYRectangle class];
            } else if (dictionary[@"y2"] != nil) {
                return [YYLine class];
            } else {
                return [self class];
            }
        }

        @end

然后就是最核心的代码了,yy_modelSetWithDictionary把字典设置到模型里面。
keyMappedCount就是_keyMappedCount = _allPropertyMetas.count;就是需要转换的属性_allPropertyMetas,经过有无读写方法,有无白黑名单处理的。
ModelSetContext包括了modelMeta,model,dictionary三个指针。
这时候可能会用到一些知识点,指针的东西可以看看,ARC 类型转换:显示转换 id 和 void *。
跟着yy_modelSetWithDictionary方法一行一行往下看,一些不容易懂的会列出来。

  • modelCustomWillTransformFromDictionary是字典转换前的处理。
  • CFDictionaryApplyFunction
    • ModelSetWithDictionaryFunction
      根据json的key从_mapper中找出对应的propertyMeta,ModelSetValueForProperty根据具体的对象model和json的value值,通过objc_msgSend方法、_setter选择器、value来赋值,遍历_next,需要注意的是结构体并不是通过objc_msgSend来发消息,所以用到了setvalue就是kvc的方式来赋值。如果有说明是多个属性名对象同一个json的key值。然后细讲ModelSetValueForProperty真正的赋值。

ModelSetValueForProperty

ModelSetValueForProperty中_isCNumber其实就是根据属性类型来判断的。

  1. 如果是_isCNumber,则通过YYNSNumberCreateFromID把id转为NSNumber,ModelSetNumberToProperty根据NSNumber通过objc_msgSend和meta->_setter设置到模型。可能需要看的东西。char,short ,int ,long,long long,unsigned long long数据范围
    uint8_t / uint16_t / uint32_t /uint64_t 是什么数据类型
  2. 其它基本上可以看
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);

这个方法为什么会有那么多情况呢?是因为做了大量的容错处理,比如json里面是数字,而对应的属性是字符串,这样会自动兼容过去。

需要注意的是Objective-C 中的NSValue的详解

一个NSValue对象是用来存储一个C或者Objective-C数据的简单容器。它可以保存任意类型的数据,比如int,float,char,当然也可以是指pointers, structures, and object ids。NSValue类的目标就是允许以上数据类型的数据结构能够被添加到集合里,例如那些需要其元素是对象的数据结构,如NSArray或者NSSet的实例。需要注意的是NSValue对象一直是不可枚举的。

需要注意的是-setValue:forKey:的方法:

如果方法的参数类型是NSNumber或NSValue的对应的基本类型,先把它转换为基本数据类,再执行方法,传入转换后的数据.
一些需要知道的东西,如果一个对象保存的是个结构体,那么通过valueForKey方式取出来的是个NSValue。
相对应的,如果一个对象的属性是个结构体,也是可以通过setValue的方法来赋值,不过我想NSValue应该是需要保持一致的。
由于结构体并不是底层调用并不是objc_msgSend,所以不能使用objc_msgSend的方法调用,而改成了kvc的方法获取,应该是可以通过objc_msgSend_stret来调用的,不过相应资料也比较少,作者也并没有使用objc_msgSend_stret的方式调用。
第十一条理解objc_masgSend的作用

一些类型编码情况:


YYModel细读二(易读)_第2张图片
image.png
  • CFArrayApplyFunction
    • ModelSetWithPropertyMetaArrayFunction是先获取value,再通过ModelSetValueForProperty赋值。
      YYValueForMultiKeys根据multi key (or key path)来获取对应的值,如果都是字符串的话,通过value = dic[key];直接按顺序访问就好;如果是数组的话,还需要则按照YYValueForKeyPath,keypath的方式访问。这样就获取到了需要设置的value。最后还是通过ModelSetValueForProperty方法来赋值。

一些容错处理的方法:

  • YYNSNumberCreateFromID,把null,yes之类的变成对应的数字。
  • YYNSDateFromString 把常见格式的字符串自动转日期
  • YYNSBlockClass 获取block,不是数据也可以设置
其它一些细节:

yy_modelEncodeWithCoder 是为了便于归档
需要注意的是:

  1. encodeObject的对象必须是实现encodeWithCoder,也就是对象必须遵循NSCoding协议的两个方法encodeWithCoder和initWithCoder。
  2. 选择器SEL需要转为字符串,encodeObject需要是对象。
  3. 必须为kvc支持的(即_isKVCCompatible为true),不支持的如下
        /*
         KVC invalid type:
         long double
         pointer (such as SEL/CoreFoundation object)
         */
  1. 作者说是NSKeyedUnarchiver只支持部分类型为结构体的NSValue对象,_isStructAvailableForKeyedArchiver,是不是这样还有待验证。

性能极大优化的有三点

  • 对信息类的缓存,每次都会优先读缓存,有需要才更新,不然不再更新,直接取缓存
  • 使用objc_msgSend调用,很多判断属性,减少在后面调用时的计算。
  • 控制信号量,避免重复解析类,dispatch_semaphore_signal和dispatch_semaphore_wait结合使用。具体在YYModel细读一结尾有写。附上图:
    YYModel细读二(易读)_第3张图片
    image.png

yy_modelInitWithCoder是个类似的就不讲了

你可能感兴趣的:(YYModel细读二(易读))