经典iOS第三方库源码分析 - YYModel
YYModel介绍
YYModel是一个针对iOS/OSX平台的高性能的Model解析库,是属于YYKit的一个组件,创建是ibireme。
其实在YYModel出现之前,已经有非常多的Model解析库,例如JSONModel、Mantle和MJExtension。
YYModel从易用性和性能方面均达到了最高水平。
性能
特性
- High performance: The conversion performance is close to handwriting code.
- Automatic type conversion: The object types can be automatically converted.
- Type Safe: All data types will be verified to ensure type-safe during the conversion process.
- Non-intrusive: There is no need to make the model class inherit from other base class.
- Lightwight: This library contains only 5 files.
- Docs and unit testing: 100% docs coverage, 99.6% code coverage.
YYModel使用
简单Model和JSON转换
// JSON:
{
"uid":123456,
"name":"Harry",
"created":"1965-07-31T00:00:00+0000"
}
// Model:
@interface User : NSObject
@property UInt64 uid;
@property NSString *name;
@property NSDate *created;
@end
@implementation User
@end
// Convert json to model:
User *user = [User yy_modelWithJSON:json];
// Convert model to json:
NSDictionary *json = [user yy_modelToJSONObject];
内嵌Model
// JSON
{
"author":{
"name":"J.K.Rowling",
"birthday":"1965-07-31T00:00:00+0000"
},
"name":"Harry Potter",
"pages":256
}
// Model: (no need to do anything)
@interface Author : NSObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Author
@end
@interface Book : NSObject
@property NSString *name;
@property NSUInteger pages;
@property Author *author;
@end
@implementation Book
@end
集合类型 - Array、Set
@class Shadow, Border, Attachment;
@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array<Shadow>
@property NSSet *borders; //Set<Border>
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
@end
@implementation Attributes
+ (NSDictionary *)modelContainerPropertyGenericClass {
// value should be Class or Class name.
return @{@"shadows" : [Shadow class],
@"borders" : Border.class,
@"attachments" : @"Attachment" };
}
@end
YYModel代码结构
YYModel整个项目非常简洁,只有5个文件。
文件 | 描述 |
---|---|
NSObject+YYModel | YYModel对于NSObject的扩展 |
YYClassInfo | 类信息 |
YYModel.h | YYModel的头文件 |
详细分析
以一个例子来分析,外部是Book对象,内部有一个Author对象。
NSString *json = @"{ \
\"author\":{ \
\"name\":\"J.K.Rowling\", \
\"birthday\":\"1965-07-31T00:00:00+0000\" \
}, \
\"name\":\"Harry Potter\", \
\"pages\":256 \
}";
Book *book = [Book yy_modelWithJSON:json];
yy_modelWithJSON
入口从[NSObject yy_modelWithJSON]进入
+ (instancetype)yy_modelWithJSON:(id)json {
NSDictionary *dic = [self _yy_dictionaryWithJSON:json];
return [self yy_modelWithDictionary:dic];
}
_yy_dictionaryWithJSON:将JSON的数据(String或者NSData)转换成NSDictionary,主要使用系统方法[NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
yy_modelWithDictionary
+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
...
Class cls = [self class];
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
if (modelMeta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
}
NSObject *one = [cls new];
if ([one yy_modelSetWithDictionary:dictionary]) return one;
return nil;
}
modelCustomClassForDictionary - Model类可以重载这个方法,将JSON转换成另外一个Model类
后续处理都放在了yy_modelSetWithDictionary这个方法
yy_modelSetWithDictionary
首先根据Class信息构造出_YYModelMeta
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
_YYModelMeta中包含如下属性:
- YYClassInfo *_classInfo:类信息,例如class、superclass、ivarInfo、methodInfos、propertyInfos
- NSDictionary *_mapper:属性key和对应的_YYModelPropertyMeta
{
author = "<_YYModelPropertyMeta: 0x6080000f5c00>";
name = "<_YYModelPropertyMeta: 0x6080000f5b00>";
pages = "<_YYModelPropertyMeta: 0x6080000f5b80>";
}
看下Name里面对应的_YYModelPropertyMeta的内容:

* _name: 对应的是property的名字
* _nsType:对应property的类型
* _getter:getter方法
* _setter:setter方法
- NSArray *_allPropertyMetas:所有的_YYModelPropertyMeta
- NSArray *_keyPathPropertyMetas:Array<_YYModelPropertyMeta>, property meta which is mapped to a key path
- NSArray *_multiKeysPropertyMetas:Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys.
数据填充
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);
}
CFDictionaryApplyFunction/CFArrayApplyFunction:针对NSDictionary和NSArray的每一个值,执行一个方法。Context作为方法中一个参数,带入了Model的信息。
Context数据结构如下:
typedef struct {
void *modelMeta; ///< _YYModelMeta
void *model; ///< id (self)
void *dictionary; ///< NSDictionary (json)
} ModelSetContext;
ModelSetWithDictionaryFunction
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;
};
}
这个方法是将Dictionary的数据填充到Model的核心过程。
通过Context获取meta(Model的类信息),通过meta获取当前Key的propertyMeta(属性信息),递归调用ModealSetValueForProperty填充model里面对应Key的Property。
ModelSetValueForProperty
这个方法会将数据填充到Model对应的Property中。
对于普通数据类型的数据填充,大体如下:
switch (meta->_nsType) {
case YYEncodingTypeNSString:
case YYEncodingTypeNSMutableString: {
if ([value isKindOfClass:[NSString class]]) {
if (meta->_nsType == YYEncodingTypeNSString) {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
} else {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy);
}
}
对于内嵌的对象属性,处理如下:
Value通常是一个NSDicationary,如果有getter方法,获取这个property的对象,如果为nill则创建一个实例,再通过[one yy_modelSetWithDictionary:value],填充这个property对象。
case YYEncodingTypeObject: {
...
else if ([value isKindOfClass:[NSDictionary class]]) {
NSObject *one = nil;
if (meta->_getter) {
one = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter);
}
if (one) {
[one yy_modelSetWithDictionary:value];
} else {
Class cls = meta->_cls;
if (meta->_hasCustomClassFromDictionary) {
cls = [cls modelCustomClassForDictionary:value];
if (!cls) cls = meta->_genericCls; // for xcode code coverage
}
one = [cls new];
[one yy_modelSetWithDictionary:value];
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)one);
}
}
} break;
最佳实践
force_inline
在YYModel实现中大量使用force_inline关键词来修饰方法,inline的作用可以参考Wikipedia: Inline Function。Inline Function会在编译阶段将方法实现直接拷贝到调用处,减少方法参数传递和查找,可以提高运行效率。
YYMode的使用方法如下:
#define force_inline __inline__ __attribute__((always_inline))
static force_inline YYEncodingNSType YYClassGetNSType(Class cls) {
...
}
一次性初始化
对于一次性初始化的代码尽量放在dispatch_once block中,保证只会初始化一次。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
lock = dispatch_semaphore_create(1);
});
Lock
通过Lock来保证多线程执行的一致性
static dispatch_semaphore_t lock;
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// do something
dispatch_semaphore_signal(lock);
缓存的实现
通过CFDictionaryCreateMutable实现了一个简易的文件缓存,注意在读取和写入缓存的时候都使用了Lock来保证多线程一致性。
+ (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;
}
总结
YYModel是一个非常简洁、高性能的Model解析库,作者使用了大量的runtime方式解析class内部信息,使用了inline、缓存、Lock等方式提高了性能和安全性。
多读经典的开源库,理解作者的实现方式,对于提高iOS设计和编程能力有很大的帮助。
经典iOS第三方库源码分析 - YYModel的更多相关文章
- iOS常用框架源码分析
SDWebImage NSCache 类似可变字典,线程安全,使用可变字典自定义实现缓存时需要考虑加锁和释放锁 在内存不足时NSCache会自动释放存储的对象,不需要手动干预 NSCache的key不 ...
- 对MBProgressHUD第三方进行源码分析
GitHub源码地址,及时更新:iOS-Source-Code-Analyze MBProgressHUD是一个为iOS app添加透明浮层 HUD 的第三方框架.作为一个 UI 层面的框架,它的实现 ...
- cJSON库源码分析
本文采用以下协议进行授权: 自由转载-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,转载请注明作者及出处. cJSON是一个超轻巧,携带方便,单文件,简单的可以作为A ...
- Redis网络库源码分析(1)之介绍篇
一.前言 Redis网络库是一个单线程EPOLL模型的网络库,和Memcached使用的libevent相比,它没有那么庞大,代码一共2000多行,因此比较容易分析.其实网上已经有非常多有关这个网络库 ...
- springBoot集成Redis遇到的坑(择库)源码分析为什么择库失败
提示: springboot提供了一套链接redis的api,也就是个jar包,用到的连接类叫做LettuceConnectionConfiguration,所以我们引入pom时是这样的 <de ...
- AspNetCore.AsyncInitialization库源码分析
AspNetCore.AsyncInitialization 这个库是用来实现在asp.net core应用程序启动时异步执行异步任务.可参考:如何在ASP.NET Core程序启动时运行异步任务(2 ...
- Redis事件库源码分析
由于老大在新项目中使用redis的事件库代替了libevent,我也趁着机会读了一遍redis的事件库代码,第一次读到“优美,让人愉快”的代码,加之用xmind制作的类图非常帅,所以留文纪念. Red ...
- Redis网络库源码分析(3)之ae.c
一.aeCreateEventLoop & aeCreateFileEvent 上一篇文章中,我们已经将服务器启动,只是其中有些细节我们跳过了,比如aeCreateEventLoop函数到底做 ...
- Redis网络库源码分析(2)之启动服务器
一.从main开始 main函数定义在server.c中,它的内容如下: //server.c int main() { signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信号 ...
随机推荐
- 2017 CCPC 杭州 流水账
day0: 队内训练ccpc 秦皇岛,敝校自己出的题,感觉一个星期没怎么写代码,手生得很,不出意料被打飞了. day1 (热身赛): 热身赛还算顺利,A题看有的队几分钟就草过去了,还以为又是西安ICP ...
- PARSEC測试集的应用领域和working set的大小
參考:tp=&arnumber=4636090">PARSEC vs. SPLASH-2: A Quantitative Comparison of Two Multithre ...
- 【Raspberry pi】GPIO注意事项
1.GPIO编码的方法 第三列是树莓派板子上的自然编号(左边引脚为1-15,右边引脚为2-26),RPi.GPIO.setmode(GPIO.BOARD)采用这列编号 树莓派主芯片提供商Broadco ...
- 三角矩阵怎么用MathType输入
虽然现在已经是暑假,但还是有很多学霸们在炎炎夏日中努力奋战,连暑假都不放过.也许正在实验室里面做得昏天暗地,也许是正在跟数据努力奋战,也许还在办公室里面一点一点地码着论文.码论文的时候,不时地要敲着复 ...
- js获取当前日期事件(yyyy-MM-dd HH:mm:ss)
function getDataTimeNow() { var nowstr = new Date(); var datenow = nowstr.getFullYear() + "-&qu ...
- 如何看懂ORACLE执行计划
如何看懂Oracle执行计划 一.什么是执行计划 An explain plan is a representation of the access path that is taken when a ...
- Access 2010 VBA 读取 表中的数据
Option Compare Database Private Sub Command0_Click() Dim db Dim rs As Recordset Dim str As String Se ...
- where case 使用
1 traceroleid表数据 tracerleid 表放着角色的相关信息, 角色ID 角色类型 密码 2 traceaccountmap表数据 表中存放着客户号和 其他角色的关系 tracerol ...
- 单源最短距离 Single Source Shortest Path
单源最短距离_示例程序_图模型_用户指南_MaxCompute-阿里云 https://help.aliyun.com/document_detail/27907.html 单源最短距离 更新时间:2 ...
- 指定文件夹 指定文件后缀名 删除整个文件夹 git 冲突解决 create a new repository on the command line push an existing repository from the command line
http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/001375840038939c2 ...