YYModel 源码剖析:关注性能

系列文章:
YYText 源码剖析:CoreText 与异步绘制
YYAsyncLayer 源码剖析:异步绘制
YYCache 源码剖析:一览亮点
YYModel 源码剖析:关注性能
YYImage 源码剖析:图片处理技巧
YYWebImage 源码剖析:线程处理与缓存策略

前言

json与模型的转换框架很多,YYModel 一出,性能吊打同类组件,终于找了些时间观摩了一番,确实收益颇多,写下此文作为分享。

由于该框架代码比较多,考虑到突出重点,压缩篇幅,不会有太多笔墨在基础知识上,很多展示源码部分会做删减,重点是在理解作者思维。读者需要具备一定的 runtime 知识,若想阅读起来轻松一些,最好自己打开源码做参照。

源码基于 1.0.4 版本。

一、框架的核心思路

使用过框架的朋友应该很熟悉如下的这些方法:

@interface NSObject (YYModel)
+ (nullable instancetype)yy_modelWithJSON:(id)json;
+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;
- (nullable id)yy_modelToJSONObject;
- (nullable NSData *)yy_modelToJSONData;
......

框架解决的问题,就是实现 jsonOC对象 间的转换,这个过程的核心问题就是 json数据OC对象的成员变量 之间的映射关系。

而这个映射关系,需要借助 runtime 来完成。只需要传入一个 Class 类变量,框架内部就能通过 runtime 将该类的属性以及方法查找出来,默认是将属性名作为映射的 key,然后 json 数据就能通过这个映射的 key 匹配赋值(通过 objc_msgSend)。

若将 OC 对象转换成 json 数据,只需要逆向处理一下。

框架做的事情说起来是简单的,不同开源库实现的细节虽然不同,但是它们的核心思路很相似。

二、类型编码 Type-Encoding

前面笔者提到,可以通过 runtime 获取到某个类的所有属性名字,达成映射。但是考虑到我们的 模型类 往往会定义很多种类型,比如:double、char、NSString、NSDate、SEL 、NSSet 等,所以需要将元数据 json(或者字典数据)转换成我们实际需要的类型。

但是,计算机如何知道我们定义的 模型类 的属性是什么类型的呢?由此,引入类型编码的概念——

两个关于类型编码的官方文档:
文档一
文档二

Type-Encoding 是指定的一套类型编码,在使用 runtime 获取某个类的成员变量、属性、方法的时候,能同时获取到它们的类型编码,通过这个编码就能辨别这些成员变量、属性、方法的数据类型(也包括属性修饰符、方法修饰符等)。

枚举的处理

关于类型编码的具体细节请自行查阅文档,本文不做讲解。在 YYModel 的源码中,作者使用了一个枚举来对应不同的类型,见名知意,方便在框架中使用:

typedef NS_OPTIONS(NSUInteger, YYEncodingType) {
    YYEncodingTypeMask       = 0xFF, ///< mask of type value
    YYEncodingTypeUnknown    = 0, ///< unknown
    YYEncodingTypeVoid       = 1, ///< void
    ......
    YYEncodingTypeCArray     = 22, ///< char[10] (for example)
    
    YYEncodingTypeQualifierMask   = 0xFF00,   ///< mask of qualifier
    YYEncodingTypeQualifierConst  = 1 << 8,  ///< const
    YYEncodingTypeQualifierIn     = 1 << 9,  ///< in
    ......
    YYEncodingTypeQualifierOneway = 1 << 14, ///< oneway
    
    YYEncodingTypePropertyMask         = 0xFF0000, ///< mask of property
    YYEncodingTypePropertyReadonly     = 1 << 16, ///< readonly
    YYEncodingTypePropertyCopy         = 1 << 17, ///< copy
    ......
    YYEncodingTypePropertyDynamic      = 1 << 23, ///< @dynamic
};

笔者并不是想把所有类型编码贴出来看,所以做了省略。这个枚举可能是多选的,所以使用了 NS_OPTIONS 而不是 NS_ENUM(编码规范)。

可以看到该枚举既包含了单选枚举值,也包含了多选枚举值,如何让它们互不影响?

作者通过YYEncodingTypeMask、YYEncodingTypeQualifierMask、YYEncodingTypePropertyMask 三个掩码将枚举值分为三部分,它们的值转换为二进制分别为:

0000 0000 0000 0000 1111 1111
0000 0000 1111 1111 0000 0000
1111 1111 0000 0000 0000 0000

然后,这三部分其他枚举的值,恰巧分布在这三个 mask 枚举的值分成的三个区间。在源码中,会看到如下代码:

YYEncodingType type;
if ((type & YYEncodingTypeMask) == YYEncodingTypeVoid) {...}

通过一个 位与& 运算符,直接将高于 YYEncodingTypeMask 的值过滤掉,然后实现单值比较。

这是一个代码技巧,挺有意思。

关于 Type-Encoding 转换 YYEncodingType 枚举的代码就不解释了,基本上根据官方文档来的。

三、将底层数据装进中间类

在 YYClassInfo 文件中,可以看到有这么几个类:

YYClassIvarInfo
YYClassMethodInfo
YYClassPropertyInfo
YYClassInfo

很明显,他们是将 Ivar、Method、objc_property_t、Class 的相关信息装进去,这样做一是方便使用,二是为了做缓存。

在源码中可以看到:
操作 runtime 底层类型的时候,由于它们不受 ARC 自动管理内存,所以记得用完了释放(但是不要去释放 const 常量),释放之前切记判断该内存是否存在防止意外crash。

基本的转换过程很简单,不一一讨论,下面提出一些值得注意的地方:

属性协议的缓存

@implementation YYClassPropertyInfo
- (instancetype)initWithProperty:(objc_property_t)property {
    ...
    NSScanner *scanner = [NSScanner scannerWithString:_typeEncoding];
...
    NSMutableArray *protocols = nil;
    while ([scanner scanString:@"<" intoString:NULL]) {
        NSString* protocol = nil;
        if ([scanner scanUpToString:@">" intoString: &protocol]) {
            if (protocol.length) {
                if (!protocols) protocols = [NSMutableArray new];
                [protocols addObject:protocol];
            }
        }
        [scanner scanString:@">" intoString:NULL];
    }
    _protocols = protocols;
...
}
...

这里作者将属性的协议同样存储起来,在后文会描述这些协议的作用。

YYClassInfo 结构

@interface YYClassInfo : NSObject
@property (nonatomic, assign, readonly) Class cls; ///< class object
@property (nullable, nonatomic, assign, readonly) Class superCls; ///< super class object
@property (nullable, nonatomic, assign, readonly) Class metaCls;  ///< class's meta class object
@property (nonatomic, readonly) BOOL isMeta; ///< whether this class is meta class
@property (nonatomic, strong, readonly) NSString *name; ///< class name
@property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< super class's class info
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; ///< ivars
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< methods
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; ///< properties
...

可以看到,Class 类的成员变量、属性、方法分别装入了三个 hash 容器(ivarInfos/methodInfos/propertyInfos)。

superClassInfo 指向父类,初始化时框架会循环向上查找,直至当前 Class 的父类不存在(NSObject 父类指针为 nil),这类似一个单向的链表,将有继承关系的类信息全部串联起来。这么做的目的,就是为了 json 转模型的时候,同样把父类的属性名作为映射的 key。初始化 YYClassInfo 的代码大致如下:

- (instancetype)initWithClass:(Class)cls {
    if (!cls) return nil;
    self = [super init];
    ...
//_update方法就是将当前类的成员变量列表、属性列表、方法列表转换放进对应的 hash
    [self _update];
//获取父类信息。 classInfoWithClass: 是一个获取类的方法,里面有缓存机制,下一步会讲到
    _superClassInfo = [self.class classInfoWithClass:_superCls];
    return self;
}

YYClassInfo 缓存

作者做了一个类信息(YYClassInfo)缓存的机制:

+ (instancetype)classInfoWithClass:(Class)cls {
    if (!cls) return nil;
//初始化几个容器和锁
    static CFMutableDictionaryRef classCache;
    static CFMutableDictionaryRef metaCache;
    static dispatch_once_t onceToken;
    static dispatch_semaphore_t lock;
    dispatch_once(&onceToken, ^{
        classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        lock = dispatch_semaphore_create(1);
    });
//读取缓存
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls));
//更新成员变量列表、属性列表、方法列表
    if (info && info->_needUpdate) [info _update];
    dispatch_semaphore_signal(lock);
//若无缓存,将 Class 类信息转换为新的 YYClassInfo 实例,并且放入缓存
    if (!info) {
        info = [[YYClassInfo alloc] initWithClass:cls];
        if (info) {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
            dispatch_semaphore_signal(lock);
        }
    }
    return info;
}

由于同一个类的相关信息在程序运行期间通常是相同的,所以使用 classCache(类hash) 和 metaCache(元类hash) 缓存已经通过 runtime 转换为 YYClassInfo 的 Class,保证不会重复转换 Class 类信息做无用功;考虑到 runtime 带来的动态特性,作者使用了一个 bool 值判断是否需要更新成员变量列表、属性列表、方法列表,_update方法就是重新获取这些信息。

这个缓存机制能带来很高的效率提升,是 YYModel 一个比较核心的操作。

有几个值得注意和学习的地方:

  1. 使用 static 修饰局部变量提升其生命周期,而又不改变其作用域,保证在程序运行期间局部变量不会释放,又防止了其他代码对该局部变量的访问。
  2. 线程安全的考虑。在初始化 static 变量的时候,使用dispatch_once()保证线程安全;在读取和写入使用 dispatch_semaphore_t信号量保证线程安全。

四、一些工具方法

在进入核心业务之前,先介绍一些 NSObject+YYModel.m 里面值得注意的工具方法。

在工具方法中,经常会看到这么一个宏来修饰函数:

#define force_inline __inline__ __attribute__((always_inline))

它的作用是强制内联,因为使用 inline 关键字最终会不会内联还是由编译器决定。对于这些强制内联的函数参数,作者经常使用 __unsafe_unretained 来修饰,拒绝其引用计数+1,以减少内存开销。

将 id 类型转换为 NSNumber

static force_inline NSNumber *YYNSNumberCreateFromID(__unsafe_unretained id value) {
    static NSCharacterSet *dot;
    static NSDictionary *dic;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dot = [NSCharacterSet characterSetWithRange:NSMakeRange('.', 1)];
        dic = @{@"TRUE" :   @(YES),
                @"True" :   @(YES),
                @"true" :   @(YES),
                ...
                @"NIL" :    (id)kCFNull,
                @"Nil" :    (id)kCFNull,
                ...
    });
    
    if (!value || value == (id)kCFNull) return nil;
    if ([value isKindOfClass:[NSNumber class]]) return value;
    if ([value isKindOfClass:[NSString class]]) {
        NSNumber *num = dic[value];
        if (num) {
            if (num == (id)kCFNull) return nil;
            return num;
        }
        ...
    return nil;
}

这里的转换处理的主要是 NSString 到 NSNumber 的转换,由于服务端返回给前端的 bool 类型、空类型多种多样,这里使用了一个 hash 将所有的情况作为 key 。然后转换的时候直接从 hash 中取值,将查找效率最大化提高。

NSString 转换为 NSDate

static force_inline NSDate *YYNSDateFromString(__unsafe_unretained NSString *string) {
    typedef NSDate* (^YYNSDateParseBlock)(NSString *string);
    #define kParserNum 34
    static YYNSDateParseBlock blocks[kParserNum + 1] = {0};
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        ...
        { /*
             Fri Sep 04 00:12:21 +0800 2015 // Weibo, Twitter
             Fri Sep 04 00:12:21.000 +0800 2015
             */
            NSDateFormatter *formatter = [NSDateFormatter new];
            formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
            formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";

            NSDateFormatter *formatter2 = [NSDateFormatter new];
            formatter2.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
            formatter2.dateFormat = @"EEE MMM dd HH:mm:ss.SSS Z yyyy";

            blocks[30] = ^(NSString *string) { return [formatter dateFromString:string]; };
            blocks[34] = ^(NSString *string) { return [formatter2 dateFromString:string]; };
        }
    });
    if (!string) return nil;
    if (string.length > kParserNum) return nil;
    YYNSDateParseBlock parser = blocks[string.length];
    if (!parser) return nil;
    return parser(string);
    #undef kParserNum
}

在 NSDictionary 原数据转模型的时候,会有将时间格式编码的字符串原数据转成 NSDate 类型的需求。

此处作者有个巧妙的设计 —— blocks。它是一个长度为 kParserNum + 1 的数组,里面的元素是YYNSDateParseBlock 类型的闭包。

作者将几乎所有(此处代码有删减)的关于时间的字符串格式罗列出来,创建等量 NSDateFormatter 对象和闭包对象,然后将 NSDateFormatter 对象 放入闭包对象的代码块中返回转换好的 NSDate 类型,最后将闭包对象放入数组,而放入的下标即为字符串的长度

实际上这也是 hash 思想,当传入有效时间格式的 NSString 对象时,通过其长度就能直接取到 blocks 数组中的闭包对象,调用闭包传入该字符串就能直接得到转换后的 NSDate 对象。

最后使用 #undef 解除 kParserNum 宏定义,避免外部的宏冲突。

获取 NSBlock 类

static force_inline Class YYNSBlockClass() {
    static Class cls;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        void (^block)(void) = ^{};
        cls = ((NSObject *)block).class;
        while (class_getSuperclass(cls) != [NSObject class]) {
            cls = class_getSuperclass(cls);
        }
    });
    return cls; // current is "NSBlock"
}

NSBlock 是 OC 中闭包的隐藏跟类(继承自 NSObject),先将一个闭包强转为 NSObject 获取其 Class 类型,然后循环查找父类,直到该 Class 的父类为 NSObject.class。

五、辅助类 _YYModelPropertyMeta

位于 NSObject+YYModel.m 中的辅助类 _YYModelPropertyMeta 是基于之前提到的 YYClassPropertyInfo 的二次解析封装,结合属性归属类添加了很多成员变量来辅助完成框架的核心业务功能,先来看一下它的结构:

@interface _YYModelPropertyMeta : NSObject {
    @package
    NSString *_name;             ///< property's name
    YYEncodingType _type;        ///< property's type
    YYEncodingNSType _nsType;    ///< property's Foundation type
    BOOL _isCNumber;             ///< is c number type
    Class _cls;                  ///< property's class, or nil
    Class _genericCls;           ///< container's generic class, or nil if threr's no generic class
    SEL _getter;                 ///< getter, or nil if the instances cannot respond
    SEL _setter;                 ///< setter, or nil if the instances cannot respond
    BOOL _isKVCCompatible;       ///< YES if it can access with key-value coding
    BOOL _isStructAvailableForKeyedArchiver; ///< YES if the struct can encoded with keyed archiver/unarchiver
    BOOL _hasCustomClassFromDictionary; ///< class/generic class implements +modelCustomClassForDictionary:
    
    NSString *_mappedToKey;      ///< the key mapped to
    NSArray *_mappedToKeyPath;   ///< the key path mapped to (nil if the name is not key path)
    NSArray *_mappedToKeyArray;  ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys)
    YYClassPropertyInfo *_info;  ///< property's info
    _YYModelPropertyMeta *_next; ///< next meta if there are multiple properties mapped to the same key.
}
@end

结合注释可以看明白一部分的变量的含义,个别成员变量的作用需要结合另外一个辅助类 _YYModelMeta 来解析,后面再讨论。

_isStructAvailableForKeyedArchiver: 标识如果该属性是结构体,是否支持编码,支持编码的结构体可以在源码里面去看。
_isKVCCompatible: 标识该成员变量是否支持 KVC。

在该类的初始化方法中,有如下处理:

@implementation _YYModelPropertyMeta
+ (instancetype)metaWithClassInfo:(YYClassInfo *)classInfo propertyInfo:(YYClassPropertyInfo *)propertyInfo generic:(Class)generic {
    // support pseudo generic class with protocol name
    if (!generic && propertyInfo.protocols) {
        for (NSString *protocol in propertyInfo.protocols) {
            Class cls = objc_getClass(protocol.UTF8String);
            if (cls) {
                generic = cls;
                break;
            }
        }
    }
...

propertyInfo.protocols即为之前缓存的属性的协议名,作者此处尝试将协议名转换为类,若转换成功,则说明该容器类型属性的元素类型是该协议同名的类。

这个操作看似意义不大,却是一个避免转换过程出错的优化(虽然这个优化有一些争议),看如下代码:

@protocol ModelA <NSObject>
@end

@interface ModelA : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation ModelA
@end

@interface ModelB : NSObject
@property (nonatomic, copy) NSArray<ModelA> *sub;
@end
@implementation ModelB
@end

//字典转模型
NSDictionary *dataDic = @{@"sub":@[@{@"name":@"a"}, @{@"name":@"b"}]};
ModelB *model = [ModelB yy_modelWithDictionary:dataDic];

你没有看错,如此仍然能转换成功,尽管这句代码中@property (nonatomic, copy) NSArray<ModelA> *sub;NSArray<>中是协议ModelA,而不是指针类型ModelA *

实际上这就是作者想达到的目的。当业务代码中有同名的 协议模型,在写容器的元素类型时(NSArray<Type>),开发者有可能会写错,而 YYModel 强行纠正了你的错误代码。

嗯。。其实笔者不是很赞成这种做法,这会让后来者包括开发者都懵逼(如果他不了解 YYModel 的实现的话)。

六、辅助类 _YYModelMeta

_YYModelMeta 是核心辅助类:

@interface _YYModelMeta : NSObject {
    @package
    YYClassInfo *_classInfo;
    /// Key:mapped key and key path, Value:_YYModelPropertyMeta.
    NSDictionary *_mapper;
    /// Array<_YYModelPropertyMeta>, all property meta of this model.
    NSArray *_allPropertyMetas;
    /// Array<_YYModelPropertyMeta>, property meta which is mapped to a key path.
    NSArray *_keyPathPropertyMetas;
    /// Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys.
    NSArray *_multiKeysPropertyMetas;
    /// The number of mapped key (and key path), same to _mapper.count.
    NSUInteger _keyMappedCount;
    /// Model class type.
    YYEncodingNSType _nsType;
    
    BOOL _hasCustomWillTransformFromDictionary;
    BOOL _hasCustomTransformFromDictionary;
    BOOL _hasCustomTransformToDictionary;
    BOOL _hasCustomClassFromDictionary;
}
@end

_classInfo 记录的 Class 信息;_mapper/_allPropertyMetas是记录属性信息(_YYModelPropertyMeta)的 hash 和数组;_keyPathPropertyMetas/_multiKeysPropertyMetas是记录属性映射为路径和映射为多个 key 的数组;_nsType 记录当前模型的类型;最后四个 bool 记录是否有自定义的相关实现。

下面将 _YYModelMeta 类初始化方法分块讲解(建议打开源码对照)。

黑名单/白名单

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
    // 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];
        }
    }
...

YYModel 是包含了众多自定义方法的协议,modelPropertyBlacklistmodelPropertyWhitelist 分别为黑名单和白名单协议方法。

自定义容器元素类型

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
// Get container property's generic class
    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)) {
                    tmp[key] = obj;
                } else if ([obj isKindOfClass:[NSString class]]) {
                    Class cls = NSClassFromString(obj);
                    if (cls) {
                        tmp[key] = cls;
                    }
                }
            }];
            genericMapper = tmp;
        }
    }
...

同样是 YYModel 协议下的方法:modelContainerPropertyGenericClass,返回了一个自定义的容器与内部元素的 hash。比如模型中一个容器属性 @property NSArray *arr;,当你希望转换过后它内部装有CustomObject类型时,你需要实现该协议方法,返回 {@"arr":@"CustomObject"} 或者 @{@"arr": CustomObject.class}(看上面代码可知作者做了兼容)。

当然,你可以指定模型容器属性的元素,如:@property NSArray<CustomObject *> *arr;

查找该类的所有属性

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
    YYClassInfo *curClassInfo = classInfo;
//循环查找父类属性,但是忽略跟类 (NSObject/NSProxy)
    while (curClassInfo && curClassInfo.superCls != nil) { // recursive parse super class, but ignore root class (NSObject/NSProxy)
        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]];
            ...
//记录
            allPropertyMetas[meta->_name] = meta;
        }
//指针向父类推进
        curClassInfo = curClassInfo.superClassInfo;
    }
...

自定义映射关系

@implementation _YYModelMeta
- (instancetype)initWithClass:(Class)cls {
...
    if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
        NSDictionary *customMapper = [(id <YYModel>)cls modelCustomPropertyMapper];
//遍历自定义映射的 hash
        [customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *mappedToKey, BOOL *stop) {
            _YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName];
            if (!propertyMeta) return;
            [allPropertyMetas removeObjectForKey:propertyName];
            
            if ([mappedToKey isKindOfClass:[NSString class]]) {
                if (mappedToKey.length == 0) return;
                propertyMeta->_mappedToKey = mappedToKey;
                //1、判断是否是路径
                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) {
                    propertyMeta->_mappedToKeyPath = keyPath;
                    [keyPathPropertyMetas addObject:propertyMeta];
                }
                //2、连接相同映射的属性
                propertyMeta->_next = mapper[mappedToKey] ?: nil;
                mapper[mappedToKey] = propertyMeta;
                
            } else if ([mappedToKey isKindOfClass:[NSArray class]]) {
                ...
            }
        }];
    }
...

modelCustomPropertyMapper 协议方法是用于自定义映射关系,比如需要将 json 中的 id 字段转换成属性:@property NSString *ID;,由于系统是默认将属性的名字作为映射的依据,所以这种业务场景需要使用者自行定义映射关系。

在实现映射关系协议时,有多种写法:

+ (NSDictionary *)modelCustomPropertyMapper {
         return @{@"name"  : @"n",
                  @"page"  : @"p",
                  @"desc"  : @"ext.desc",
                  @"bookID": @[@"id", @"ID", @"book_id"]};
}

key 是模型中的属性名字,value 就是对于 json(或字典)数据源的字段。特别的,可以使用“.”来链接字符形成一个路径,也可以传入一个数组,当映射的是一个数组的时候,json -> model 的时候会找到第一个有效的映射作为model属性的值。比如上面代码中,在数据源中找到 ID 字符,便会将其值给当前模型类的 bookID 属性,忽略掉后面的映射(book_id)。

性能层面,可以在代码中看到两个闪光点:

1、判断是否是路径

将映射的 value 拆分成 keyPath 数组,然后做了一个遍历,当遍历到 @"" 空字符值时,深拷贝一份 keyPath 移除所有的 @"" 然后 break

这个操作看似简单,实则是作者对性能的优化。通常情况下,传入的路径是正确的 a.b.c,这时不需要移除 @"" 。而当路径错误,比如 a..b.ca.b.c. 时,分离字符串时 keyPath 中就会有空值 @""。由于 componentsSeparatedByString 方法返回的是一个不可变的数组,所以移除 keyPath 中的 @"" 需要先深拷贝一份可变内存。

作者此处的想法很明显:在正常情况下,不需要移除,也就是不需要深拷贝 keyPath 增加内存开销。

若考虑到极致的性能,会发现此处做了两个遍历(一个拆分 mappedToKey 的遍历,一个 keyPath 的遍历),应该一个遍历就能做出来,有兴趣的朋友可能尝试一下。

不过此处的路径不会很长,也就基本可以忽略掉多的这几次遍历了。

2、连接相同映射的属性

之前解析 _YYModelPropertyMeta 类时,可以发现它有个成员变量 _YYModelPropertyMeta *_next;,它的作用就可以从此处看出端倪。

代码中,mapper是记录的所有属性的 hash(由前面未贴出代码得到),hash 的 key 即为映射的值(路径)。作者做了一个判断,若 mapper中存在相同 key 的属性,就改变了一下指针,做了一个链接,将相同映射 key 的属性连接起来形成一个链表。

这么做的目的很简单,就是为了在 json 数据源查找到某个目标值时,可以移动 _next 指针,将所有的相同映射的属性统统赋值,从而达到不重复查找数据源相同路径值的目的。

对象缓存

+ (instancetype)metaWithClass:(Class)cls {
    if (!cls) return nil;
    static CFMutableDictionaryRef cache;
    static dispatch_once_t onceToken;
    static dispatch_semaphore_t lock;
    dispatch_once(&onceToken, ^{
        cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        lock = dispatch_semaphore_create(1);
    });
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
    dispatch_semaphore_signal(lock);
    if (!meta || meta->_classInfo.needUpdate) {
        meta = [[_YYModelMeta alloc] initWithClass:cls];
        if (meta) {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta));
            dispatch_semaphore_signal(lock);
        }
    }
    return meta;
}

_YYModelMeta 的缓存逻辑和 上文中 YYClassInfo 的缓存逻辑一样,不多阐述。

七、给数据模型属性赋值 / 将数据模型解析成 json

实际上上文已经将 YYModel 的大部分内容讲解完了,可以说之前的都是准备工作。

NSObject+YYModel.m 中有个很长的方法:

static void ModelSetValueForProperty(__unsafe_unretained id model,
                                     __unsafe_unretained id value,
                                     __unsafe_unretained _YYModelPropertyMeta *meta) {...}

看该方法的名字应该很容易猜到,这就是将数据模型(model)中的某个属性(meta)赋值为目标值(value)。具体代码不贴了,主要是根据之前的一些辅助的类,利用 objc_msgSend 给目标数据 model 发送属性的 setter 方法。代码看起来复杂,实际上很简单。

相反地,有这样一个方法将已经赋值的数据模型解析成 json:

static id ModelToJSONObjectRecursive(NSObject *model) {...}

实现都是根据前文解析的那些中间类来处理的。

性能的优化

直接使用 objc_msgSend给对象发送消息的效率要高于使用 KVC,可以在源码中看到作者但凡可以使用发送消息赋值处理的,都不会使用 KVC。

八、从入口函数说起

回到开头,有几个方法是经常使用的(当然包括 NSArray 和 NSDictionary 中的延展方法):

+ (nullable instancetype)yy_modelWithJSON:(id)json;
+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;

这些方法其实落脚点都在一个方法:

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
    if (!dic || dic == (id)kCFNull) return NO;
    if (![dic isKindOfClass:[NSDictionary class]]) return NO;
//通过 Class 获取 _YYModelMeta 实例
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
    ...

 /*使用 ModelSetContext 结构体将以下内容装起来:
1、具体模型对象(self)  
2、通过模型对象的类 Class 转换的 _YYModelMeta 对象(modelMeta)
3、json 转换的原始数据(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)) {
        CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
        if (modelMeta->_keyPathPropertyMetas) {
            CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
                                 CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
                                 ModelSetWithPropertyMetaArrayFunction,
                                 &context);
        }
        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);
    }
    
    ...
    return YES;
}

这里使用 CF 框架下的函数是为提升执行效率。

至于 ModelSetWithPropertyMetaArrayFunctionModelSetWithDictionaryFunction 的实现不复杂,不多解析。

九、组件对外提供的一些工具方法

作者很细心的提供了一些工具方法方便开发者使用。

拷贝
- (id)yy_modelCopy;

注意是深拷贝。

归档/解档
- (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder;
- (id)yy_modelInitWithCoder:(NSCoder *)aDecoder;

喜欢用归解档朋友的福音。

hash 值
- (NSUInteger)yy_modelHash;

提供了一个现成的 hash 表算法,方便开发者构建 hash 数据结构。

判断相等
- (BOOL)yy_modelIsEqual:(id)model;

在方法实现中,当两个待比较对象的 hash 值不同时,作者使用 if ([self hash] != [model hash]) return NO; 判断来及时返回,提高比较效率。

后语

本文主要是剖析 YYModel 的重点、难点、闪光点,更多的技术实现细节请查阅源码,作者的细节处理得很棒。

从该框架中,可以看到作者对性能的极致追求,这也是作为一位合格的开发者应有的精神。不断的探究实践思考,才能真正的做好一件事。

希望本文能让读者朋友对 YYModel 有更深的理解😁。

参考文献:作者 ibireme 的博客 iOS JSON 模型转换库评测

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 导语:YYModel库是优秀的模型转换库,可自动处理模型转换(从JSON到Model 和 Model到JSON)的...
    南华coder阅读 5,423评论 0 11
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,968评论 3 119
  • (一) 纸上听来终觉浅,走进吴师须躬行。 相见恨晚通心意,吴师吾师今相逢。 (二) 倾囊相授育人论,诲人不倦赤子心...
    阿桂爱原创阅读 816评论 28 40