简介(摘至官网)
特性
- 高性能: 模型转换性能接近手写解析代码。
- 自动类型转换: 对象类型可以自动转换,详情见下方表格。
- 类型安全: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。
- 无侵入性: 模型无需继承自其他基类。
- 轻量: 该框架只有 5 个文件 (包括.h文件)。
- 文档和单元测试: 文档覆盖率100%, 代码覆盖率99.6%。
基本使用
简单的 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
// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
User *user = [User yy_modelWithJSON:json];
// 将 Model 转换为 JSON 对象:
NSDictionary *json = [user yy_modelToJSONObject];
当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。
Model 属性名和 JSON 中的 Key 不相同
// JSON:
{
"n":"Harry Pottery",
"p": 256,
"ext" : {
"desc" : "A book written by J.K.Rowing."
},
"ID" : 100010
}
// Model:
@interface Book : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
@implementation Book
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"page" : @"p",
@"desc" : @"ext.desc",
@"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end
你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。
在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。
在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。
Model 包含其他 Model
// JSON
{
"author":{
"name":"J.K.Rowling",
"birthday":"1965-07-31T00:00:00+0000"
},
"name":"Harry Potter",
"pages":256
}
// Model: 什么都不用做,转换会自动完成
@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; //Book 包含 Author 属性
@end
@implementation Book
@end
容器类属性
@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
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
return @{@"shadows" : [Shadow class],
@"borders" : Border.class,
@"attachments" : @"Attachment" };
}
@end
黑名单与白名单
@interface User
@property NSString *name;
@property NSUInteger age;
@end
@implementation Attributes
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
+ (NSArray *)modelPropertyBlacklist {
return @[@"test1", @"test2"];
}
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
+ (NSArray *)modelPropertyWhitelist {
return @[@"name"];
}
@end
数据校验与自定义转换
// JSON:
{
"name":"Harry",
"timestamp" : 1445534567
}
// Model:
@interface User
@property NSString *name;
@property NSDate *createdAt;
@end
@implementation User
// 当 JSON 转为 Model 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
NSNumber *timestamp = dic[@"timestamp"];
if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
return YES;
}
// 当 Model 转为 JSON 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
if (!_createdAt) return NO;
dic[@"timestamp"] = @(n.timeIntervalSince1970);
return YES;
}
@end
底层原理
YYModel 类结构
除掉YYModel.h之外,只剩下了YYClassInfo和NSObject+YYModel两个模块啦
-
YYClassInfo功能主要是将Runtime层级中的一些结构体封装到NSObject中调用
NSObject+YYModel功能是提供调用的接口以及实现具体的模型转换逻辑
前面已经讲到YYClassInfo主要功能是将Runtime层级的结构体封装到NSObject层级以便调用。下面是YYClassInfo与Runtime层级对比:
详解
YYClassIvarInfo
YYClassIvarInfo && objc_ivar
// 类的属性描述
@interface YYClassIvarInfo : NSObject
// 成员变量
@property (nonatomic, assign, readonly) Ivar ivar; ///< ivar opaque struct
// 变量名称
@property (nonatomic, strong, readonly) NSString *name; ///< Ivar's name
// 成员变量地址偏移量
@property (nonatomic, assign, readonly) ptrdiff_t offset; ///< Ivar's offset
// 成员变量类型编码类型
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< Ivar's type encoding
// 成员变量数据类型
@property (nonatomic, assign, readonly) YYEncodingType type; ///< Ivar's type
/**
Creates and returns an ivar info object.
@param ivar ivar opaque struct
@return A new object, or nil if an error occurs.
*/
// 初始化一个类成员变量描述
- (instancetype)initWithIvar:(Ivar)ivar;
@end
紧接着我们看一下Runtime的objc_ivar表示变量的结构体
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE; // 变量名称
char * _Nullable ivar_type OBJC2_UNAVAILABLE; // 变量类型
int ivar_offset OBJC2_UNAVAILABLE; // 变量偏移量
#ifdef __LP64__ // 如果已定义 __LP64__ 则表示正在构建 64 位目标
int space OBJC2_UNAVAILABLE; // 变量空间
#endif
}
注:日常开发中,NSString类型的属性会用copy修饰,看上面YYClassIvarInfo中typeEncoding和name是用strong修饰。这是因为其内部先是通过Runtime方法拿到const char * 之后通过 stringWithUTF8String 方法之后转为 NSString 的。所以 NSString 这类属性在确定其不会在初始化之后出现被修改的情况下,使用 strong来修饰 做一次单纯的强引用在性能上是比 copy 要高的。
YYClassMethodInfo && objc_method
下面是YYClassMethodInfo
/**
Method information.
*/
// 类方法的描述
@interface YYClassMethodInfo : NSObject
// 方法,实质上就是一个结构体
@property (nonatomic, assign, readonly) Method method; ///< method opaque struct
// 方法名
@property (nonatomic, strong, readonly) NSString *name; ///< method name
// 方法的选择子,实质上是可以跟 name 进行转换的
@property (nonatomic, assign, readonly) SEL sel; ///< method's selector
// 函数的实现
@property (nonatomic, assign, readonly) IMP imp; ///< method's implementation
// 参数的编码
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< method's parameter and return types
// 返回值的编码
@property (nonatomic, strong, readonly) NSString *returnTypeEncoding; ///< return value's type
// 所有的参数编码
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *argumentTypeEncodings; ///< array of arguments' type
/**
Creates and returns a method info object.
@param method method opaque struct
@return A new object, or nil if an error occurs.
*/
// 初始化一个函数描述
- (instancetype)initWithMethod:(Method)method;
@end
YYClassMethodInfo则是对Rutime里面的objc_method的封装,紧接着我们看Runtime的objc_method结构体
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名称
char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法类型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法实现(函数指针)
}
YYClassPropertyInfo && property_t
YYClassPropertyInfo是对Runtime中property_t的封装
/**
Property information.
*/
// 类属性的描述
@interface YYClassPropertyInfo : NSObject
// 属性
@property (nonatomic, assign, readonly) objc_property_t property; ///< property's opaque struct
// 属性名
@property (nonatomic, strong, readonly) NSString *name; ///< property's name
// 编码类型 由 typeEncoding 转换而来
@property (nonatomic, assign, readonly) YYEncodingType type; ///< property's type
// 编码类型
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< property's encoding value
// 变量名
@property (nonatomic, strong, readonly) NSString *ivarName; ///< property's ivar name
// 隶属的 class
@property (nullable, nonatomic, assign, readonly) Class cls; ///< may be nil
// 遵守的协议
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *protocols; ///< may nil
// get 方法
@property (nonatomic, assign, readonly) SEL getter; ///< getter (nonnull)
// set 方法
@property (nonatomic, assign, readonly) SEL setter; ///< setter (nonnull)
/**
Creates and returns a property info object.
@param property property opaque struct
@return A new object, or nil if an error occurs.
*/
- (instancetype)initWithProperty:(objc_property_t)property;
@end
然后来看一下Runtime的property_t结构体
struct property_t {
const char *name; // 名称
const char *attributes; // 修饰
};
YYClassInfo && objc_class
YYClassInfo封装了Runtime的objc_class,下面看一下YYClassInfo
YYClassInfo
/**
Class information for a class.
*/
// 类的描述
@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
/**
If the class is changed (for example: you add a method to this class with
'class_addMethod()'), you should call this method to refresh the class info cache.
After called this method, `needUpdate` will returns `YES`, and you should call
'classInfoWithClass' or 'classInfoWithClassName' to get the updated class info.
*/
//
// 更新
- (void)setNeedUpdate;
/**
If this method returns `YES`, you should stop using this instance and call
`classInfoWithClass` or `classInfoWithClassName` to get the updated class info.
@return Whether this class info need update.
*/
// 是否更新
- (BOOL)needUpdate;
/**
Get the class info of a specified Class.
@discussion This method will cache the class info and super-class info
at the first access to the Class. This method is thread-safe.
@param cls A class.
@return A class info, or nil if an error occurs.
*/
// 通过 class 初始化一个类描述对象
+ (nullable instancetype)classInfoWithClass:(Class)cls;
/**
Get the class info of a specified Class.
@discussion This method will cache the class info and super-class info
at the first access to the Class. This method is thread-safe.
@param className A class name.
@return A class info, or nil if an error occurs.
*/
// 通过 类名 初始化一个描述对象
+ (nullable instancetype)classInfoWithClassName:(NSString *)className;
@end
objc_class
// objc.h
typedef struct objc_class *Class;
// runtime.h
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // isa 指针
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE; // 父类(超类)指针
const char * _Nonnull name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 版本
long info OBJC2_UNAVAILABLE; // 信息
long instance_size OBJC2_UNAVAILABLE; // 初始尺寸
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 变量列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法列表
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; // 缓存
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 协议列表
#endif
} OBJC2_UNAVAILABLE;
注解:下面是Runtime关于class的知识,下图是 《iOS 进阶》对oc对象模型的解释
下面是对应的讲解。
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);
// 从元类缓存中查找类描述对象,如果是元类,如果是元类,就从元类缓存中找;如果cls 不是元类,从类缓存中找
YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls));
// 默认为NO 第一次加载 info 的时候会进入判断条件
if (info && info->_needUpdate) {
[info _update];
}
dispatch_semaphore_signal(lock);
// 如果没有查找到
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;
}
下面总结一下初始化主要步骤:
- 首先创建单例缓存,类缓存和元类缓存
- 使用dispatch_semaphore 保证缓存线程安全
- 初始化操作之前首先缓存中查找是否已经向缓存中注册过的当前要初始化的YYClassInfo
- 如果查找缓存对象,需要判断对象是否需要更新以及其他相关操作
- 如果没有找到缓存对象,就开始初始化
- 初始化成功之后,向缓存中注册YYClassInfo实例
NSObject+YYModel
NSObject+YYModel在YYModel主要任务是利用YYClassInfo层级封装的类来执行JSON模型之间的转换逻辑。下面是NSObject+YYModel讲述的主要内容:
- 类型编码的解析
- 数据结构的定义
- 递归模型的转换
- 接口相关的代码
数据结构的定义
NSObject+YYModel重新定义了两个类,来使用 YYClassInfo 中的封装。
_YYModelPropertyMeta
/// A property info in object model.
// 对象模型的属性信息
@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
// get 方法
SEL _getter; ///< getter, or nil if the instances cannot respond
// set 方法
SEL _setter; ///< setter, or nil if the instances cannot respond
// KVO 兼容属性
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:
/*
property->key: _mappedToKey:key _mappedToKeyPath:nil _mappedToKeyArray:nil
property->keyPath: _mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil
property->keys: _mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath _mappedToKeyArray:keys(array)
*/
// 映射 key
NSString *_mappedToKey; ///< the key mapped to
// 映射 keyPath,如果没有映射到 keyPath 则返回 nil
NSArray *_mappedToKeyPath; ///< the key path mapped to (nil if the name is not key path)
// key 或者 keyPath 的数组,如果没有映射多个键的话则返回 nil
NSArray *_mappedToKeyArray; ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys)
// 属性信息,详见上文 YYClassPropertyInfo && property_t 章节
YYClassPropertyInfo *_info; ///< property's info
// 如果有多个属性映射到同一个 key 则指向下一个模型属性元
_YYModelPropertyMeta *_next; ///< next meta if there are multiple properties mapped to the same key.
}
@end
_YYModelMeta
// 对象模型的类信息
@interface _YYModelMeta : NSObject {
@package
// 类描述
YYClassInfo *_classInfo;
/// Key:mapped key and key path, Value:_YYModelPropertyMeta.
// 属性映射器,包括key和keypath
NSDictionary *_mapper;
/// Array<_YYModelPropertyMeta>, all property meta of this model.
// 类属性
NSArray *_allPropertyMetas;
/// Array<_YYModelPropertyMeta>, property meta which is mapped to a key path.
// 类中的所欲keypath 属性
NSArray *_keyPathPropertyMetas;
/// Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys.
// 所有key
NSArray *_multiKeysPropertyMetas;
/// The number of mapped key (and key path), same to _mapper.count.
// key 的个数
NSUInteger _keyMappedCount;
/// Model class type.
// 类型
YYEncodingNSType _nsType;
BOOL _hasCustomWillTransformFromDictionary;
BOOL _hasCustomTransformFromDictionary;
BOOL _hasCustomTransformToDictionary;
BOOL _hasCustomClassFromDictionary;
}
@end