转载

YYModel 源码剖析:关注性能

前言

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

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

一、框架的核心思路

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

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

框架解决的问题,就是实现 json 和 OC对象 间的转换,这个过程的核心问题就是 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。

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

YYClassPropertyInfo 记录属性 <> 包裹部分

@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;
...
}
@end

在对 objc_property_t 的转换中,作者查找了类型编码中用 <> 包裹的部分,然后把它们放进一个数组。这是作者做的一个基于用户体验的优化,比如一个属性@property NSArrayarr,框架就可以解析到 arr 容器内部是包含的 CustomObject 类型,而不需使用者专门写方法去映射(当然可以自定义映射)。关于具体的逻辑后面会讲到。

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 *ivarInfos; ///< ivars
@property (nullable, nonatomic, strong, readonly) NSDictionary *methodInfos; ///< methods
@property (nullable, nonatomic, strong, readonly) NSDictionary *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;
            }
        }
    }
...

generic 即为容器的元素类型,若初始化时没有传入,这里会读取属性 <> 包含的字符,转换成对应的类。

六、辅助类 _YYModelMeta

_YYModelMeta 是核心辅助类:

@interface _YYModelMeta : NSObject {
    @package
    YYClassInfo *_classInfo;
    /// Key:mapped key and key path, Value:_YYModelPropertyMeta.
    NSDictionary *_mapper;
    /// Array, all property meta of this model.
    NSArray *_allPropertyMetas;
    /// Array, property meta which is mapped to a key path.
    NSArray *_keyPathPropertyMetas;
    /// Array, 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)cls modelPropertyBlacklist];
        if (properties) {
            blacklist = [NSSet setWithArray:properties];
        }
    }
    // Get white list
    NSSet *whitelist = nil;
    if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) {
        NSArray *properties = [(id)cls modelPropertyWhitelist];
        if (properties) {
            whitelist = [NSSet setWithArray:properties];
        }
    }
...

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

自定义容器元素类型

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

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

当然,你可以指定模型容器属性的元素,如:@property NSArray*arr;,若你未在上述协议中返回该属性的映射关系,那么在将该属性转换成中间类 _YYModelPropertyMeta 时,会自动查找属性的 type-ecoding 中的 <> 的包裹部分,从而定位你的容器里面是什么元素。(可以查看前面对 _YYModelPropertyMeta 初始化方法的解析)

查找该类的所有属性

@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 )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.c、a.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 框架下的函数是为提升执行效率。

至于ModelSetWithPropertyMetaArrayFunction和ModelSetWithDictionaryFunction的实现不复杂,不多解析。

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

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

拷贝
- (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 模型转换库评测

作者:indulge_in

链接:https://www.jianshu.com/p/fe30e6bbc551

正文到此结束
Loading...