YYKit 系列源码剖析文章:html
json与模型的转换框架不少,YYModel 一出,性能吊打同类组件,终于找了些时间观摩了一番,确实收益颇多,写下此文做为分享。前端
因为该框架代码比较多,考虑到突出重点,压缩篇幅,不会有太多笔墨在基础知识上,不少展现源码部分会作删减,重点是在理解做者思惟。读者须要具有必定的 runtime 知识,若想阅读起来轻松一些,最好本身打开源码作参照。ios
源码基于 1.0.4 版本。算法
使用过框架的朋友应该很熟悉以下的这些方法:json
@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 数据,只须要逆向处理一下。安全
框架作的事情提及来是简单的,不一样开源库实现的细节虽然不一样,可是它们的核心思路很类似。bash
前面笔者提到,能够经过 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;
...
}
...
复制代码
这里做者将属性的协议一样存储起来,在后文会描述这些协议的做用。
@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)缓存的机制:
+ (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 一个比较核心的操做。
有几个值得注意和学习的地方:
dispatch_once()
保证线程安全;在读取和写入使用 dispatch_semaphore_t
信号量保证线程安全。在进入核心业务以前,先介绍一些 NSObject+YYModel.m 里面值得注意的工具方法。
在工具方法中,常常会看到这么一个宏来修饰函数:
#define force_inline __inline__ __attribute__((always_inline))
复制代码
它的做用是强制内联,由于使用 inline 关键字最终会不会内联仍是由编译器决定。对于这些强制内联的函数参数,做者常用 __unsafe_unretained 来修饰,拒绝其引用计数+1,以减小内存开销。
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 中取值,将查找效率最大化提升。
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 宏定义,避免外部的宏冲突。
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。
位于 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),开发者有可能会写错,而 YYModel 强行纠正了你的错误代码。
嗯。。其实笔者不是很同意这种作法,这会让后来者包括开发者都懵逼(若是他不了解 YYModel 的实现的话)。
_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 是包含了众多自定义方法的协议,modelPropertyBlacklist
和 modelPropertyWhitelist
分别为黑名单和白名单协议方法。
@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;
//一、判断是不是路径
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];
}
//二、链接相同映射的属性
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
)。
性能层面,能够在代码中看到两个闪光点:
将映射的 value
拆分红 keyPath
数组,而后作了一个遍历,当遍历到 @""
空字符值时,深拷贝一份 keyPath
移除全部的 @""
而后 break
。
这个操做看似简单,实则是做者对性能的优化。一般状况下,传入的路径是正确的 a.b.c
,这时不须要移除 @""
。而当路径错误,好比 a..b.c
、a.b.c.
时,分离字符串时 keyPath
中就会有空值 @""
。因为 componentsSeparatedByString
方法返回的是一个不可变的数组,因此移除 keyPath
中的 @""
须要先深拷贝一份可变内存。
做者此处的想法很明显:在正常状况下,不须要移除,也就是不须要深拷贝 keyPath
增长内存开销。
若考虑到极致的性能,会发现此处作了两个遍历(一个拆分 mappedToKey
的遍历,一个 keyPath
的遍历),应该一个遍历就能作出来,有兴趣的朋友可能尝试一下。
不过此处的路径不会很长,也就基本能够忽略掉多的这几回遍历了。
以前解析 _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 的缓存逻辑同样,很少阐述。
实际上上文已经将 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 结构体将如下内容装起来:
一、具体模型对象(self)
二、经过模型对象的类 Class 转换的 _YYModelMeta 对象(modelMeta)
三、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;
复制代码
喜欢用归解档朋友的福音。
- (NSUInteger)yy_modelHash;
复制代码
提供了一个现成的 hash 表算法,方便开发者构建 hash 数据结构。
- (BOOL)yy_modelIsEqual:(id)model;
复制代码
在方法实现中,当两个待比较对象的 hash 值不一样时,做者使用 if ([self hash] != [model hash]) return NO;
判断来及时返回,提升比较效率。
本文主要是剖析 YYModel 的重点、难点、闪光点,更多的技术实现细节请查阅源码,做者的细节处理得很棒。
从该框架中,能够看到做者对性能的极致追求,这也是做为一位合格的开发者应有的精神。不断的探究实践思考,才能真正的作好一件事。
但愿本文能让读者朋友对 YYModel 有更深的理解😁。
参考文献:做者 ibireme 的博客 iOS JSON 模型转换库评测