簡介(摘至官網(wǎng))
特性
- 高性能: 模型轉(zhuǎn)換性能接近手寫解析代碼。
- 自動(dòng)類型轉(zhuǎn)換: 對象類型可以自動(dòng)轉(zhuǎn)換,詳情見下方表格。
- 類型安全: 轉(zhuǎn)換過程中,所有的數(shù)據(jù)類型都會被檢測一遍,以保證類型安全,避免崩潰問題。
- 無侵入性: 模型無需繼承自其他基類。
- 輕量: 該框架只有 5 個(gè)文件 (包括.h文件)。
- 文檔和單元測試: 文檔覆蓋率100%, 代碼覆蓋率99.6%。
基本使用
簡單的 Model 與 JSON 相互轉(zhuǎn)換
// 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) 轉(zhuǎn)換為 Model:
User *user = [User yy_modelWithJSON:json];
// 將 Model 轉(zhuǎn)換為 JSON 對象:
NSDictionary *json = [user yy_modelToJSONObject];
當(dāng) JSON/Dictionary 中的對象類型與 Model 屬性不一致時(shí),YYModel 將會進(jìn)行如下自動(dòng)轉(zhuǎn)換。自動(dòng)轉(zhuǎn)換不支持的值將會被忽略,以避免各種潛在的崩潰問題。
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
//返回一個(gè) Dict,將 Model 屬性名對映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"page" : @"p",
@"desc" : @"ext.desc",
@"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end
你可以把一個(gè)或一組 json key (key path) 映射到一個(gè)或多個(gè)屬性。如果一個(gè)屬性沒有映射關(guān)系,那默認(rèn)會使用相同屬性名作為映射。
在 json->model 的過程中:如果一個(gè)屬性對應(yīng)了多個(gè) json key,那么轉(zhuǎn)換過程會按順序查找,并使用第一個(gè)不為空的值。
在 model->json 的過程中:如果一個(gè)屬性對應(yīng)了多個(gè) json key (key path),那么轉(zhuǎn)換過程僅會處理第一個(gè) json key (key path);如果多個(gè)屬性對應(yīng)了同一個(gè) json key,則轉(zhuǎn)換過過程會使用其中任意一個(gè)不為空的值。
Model 包含其他 Model
// JSON
{
"author":{
"name":"J.K.Rowling",
"birthday":"1965-07-31T00:00:00+0000"
},
"name":"Harry Potter",
"pages":256
}
// Model: 什么都不用做,轉(zhuǎn)換會自動(dòng)完成
@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
// 返回容器類中的所需要存放的數(shù)據(jù)類型 (以 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
// 如果實(shí)現(xiàn)了該方法,則處理過程中會忽略該列表內(nèi)的所有屬性
+ (NSArray *)modelPropertyBlacklist {
return @[@"test1", @"test2"];
}
// 如果實(shí)現(xiàn)了該方法,則處理過程中不會處理該列表外的屬性。
+ (NSArray *)modelPropertyWhitelist {
return @[@"name"];
}
@end
數(shù)據(jù)校驗(yàn)與自定義轉(zhuǎn)換
// JSON:
{
"name":"Harry",
"timestamp" : 1445534567
}
// Model:
@interface User
@property NSString *name;
@property NSDate *createdAt;
@end
@implementation User
// 當(dāng) JSON 轉(zhuǎn)為 Model 完成后,該方法會被調(diào)用。
// 你可以在這里對數(shù)據(jù)進(jìn)行校驗(yàn),如果校驗(yàn)不通過,可以返回 NO,則該 Model 會被忽略。
// 你也可以在這里做一些自動(dòng)轉(zhuǎn)換不能完成的工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
NSNumber *timestamp = dic[@"timestamp"];
if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
return YES;
}
// 當(dāng) Model 轉(zhuǎn)為 JSON 完成后,該方法會被調(diào)用。
// 你可以在這里對數(shù)據(jù)進(jìn)行校驗(yàn),如果校驗(yàn)不通過,可以返回 NO,則該 Model 會被忽略。
// 你也可以在這里做一些自動(dòng)轉(zhuǎn)換不能完成的工作。
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
if (!_createdAt) return NO;
dic[@"timestamp"] = @(n.timeIntervalSince1970);
return YES;
}
@end
底層原理
YYModel 類結(jié)構(gòu)

除掉YYModel.h之外,只剩下了YYClassInfo和NSObject+YYModel兩個(gè)模塊啦
-
YYClassInfo功能主要是將Runtime層級中的一些結(jié)構(gòu)體封裝到NSObject中調(diào)用
1305302-20180915103000006-951413839.jpg NSObject+YYModel功能是提供調(diào)用的接口以及實(shí)現(xiàn)具體的模型轉(zhuǎn)換邏輯
前面已經(jīng)講到Y(jié)YClassInfo主要功能是將Runtime層級的結(jié)構(gòu)體封裝到NSObject層級以便調(diào)用。下面是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
// 成員變量數(shù)據(jù)類型
@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.
*/
// 初始化一個(gè)類成員變量描述
- (instancetype)initWithIvar:(Ivar)ivar;
@end
緊接著我們看一下Runtime的objc_ivar表示變量的結(jié)構(gòu)體
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE; // 變量名稱
char * _Nullable ivar_type OBJC2_UNAVAILABLE; // 變量類型
int ivar_offset OBJC2_UNAVAILABLE; // 變量偏移量
#ifdef __LP64__ // 如果已定義 __LP64__ 則表示正在構(gòu)建 64 位目標(biāo)
int space OBJC2_UNAVAILABLE; // 變量空間
#endif
}
注:日常開發(fā)中,NSString類型的屬性會用copy修飾,看上面YYClassIvarInfo中typeEncoding和name是用strong修飾。這是因?yàn)槠鋬?nèi)部先是通過Runtime方法拿到const char * 之后通過 stringWithUTF8String 方法之后轉(zhuǎn)為 NSString 的。所以 NSString 這類屬性在確定其不會在初始化之后出現(xiàn)被修改的情況下,使用 strong來修飾 做一次單純的強(qiáng)引用在性能上是比 copy 要高的。
YYClassMethodInfo && objc_method
下面是YYClassMethodInfo
/**
Method information.
*/
// 類方法的描述
@interface YYClassMethodInfo : NSObject
// 方法,實(shí)質(zhì)上就是一個(gè)結(jié)構(gòu)體
@property (nonatomic, assign, readonly) Method method; ///< method opaque struct
// 方法名
@property (nonatomic, strong, readonly) NSString *name; ///< method name
// 方法的選擇子,實(shí)質(zhì)上是可以跟 name 進(jìn)行轉(zhuǎn)換的
@property (nonatomic, assign, readonly) SEL sel; ///< method's selector
// 函數(shù)的實(shí)現(xiàn)
@property (nonatomic, assign, readonly) IMP imp; ///< method's implementation
// 參數(shù)的編碼
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< method's parameter and return types
// 返回值的編碼
@property (nonatomic, strong, readonly) NSString *returnTypeEncoding; ///< return value's type
// 所有的參數(shù)編碼
@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.
*/
// 初始化一個(gè)函數(shù)描述
- (instancetype)initWithMethod:(Method)method;
@end
YYClassMethodInfo則是對Rutime里面的objc_method的封裝,緊接著我們看Runtime的objc_method結(jié)構(gòu)體
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名稱
char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法類型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法實(shí)現(xiàn)(函數(shù)指針)
}
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 轉(zhuǎn)換而來
@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
// 遵守的協(xié)議
@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結(jié)構(gòu)體
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
// 這個(gè)類是否是元類
@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 初始化一個(gè)類描述對象
+ (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.
*/
// 通過 類名 初始化一個(gè)描述對象
+ (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; // 協(xié)議列表
#endif
} OBJC2_UNAVAILABLE;
注解:下面是Runtime關(guān)于class的知識,下圖是 《iOS 進(jìn)階》對oc對象模型的解釋
1305302-20180915150609417-1293696256.jpg
下面是對應(yīng)的講解。

YYClassInfo 的初始化
+ (instancetype)classInfoWithClass:(Class)cls {
// 如果父類不存在,則結(jié)束調(diào)用
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));
// 默認(rèn)為NO 第一次加載 info 的時(shí)候會進(jìn)入判斷條件
if (info && info->_needUpdate) {
[info _update];
}
dispatch_semaphore_signal(lock);
// 如果沒有查找到
if (!info) {
// 實(shí)例化
info = [[YYClassInfo alloc] initWithClass:cls];
if (info) {
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// 根據(jù)是否是元類屬性區(qū)分存儲
CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
dispatch_semaphore_signal(lock);
}
}
return info;
}
下面總結(jié)一下初始化主要步驟:
- 首先創(chuàng)建單例緩存,類緩存和元類緩存
- 使用dispatch_semaphore 保證緩存線程安全
- 初始化操作之前首先緩存中查找是否已經(jīng)向緩存中注冊過的當(dāng)前要初始化的YYClassInfo
- 如果查找緩存對象,需要判斷對象是否需要更新以及其他相關(guān)操作
- 如果沒有找到緩存對象,就開始初始化
- 初始化成功之后,向緩存中注冊YYClassInfo實(shí)例
NSObject+YYModel

NSObject+YYModel在YYModel主要任務(wù)是利用YYClassInfo層級封裝的類來執(zhí)行JSON模型之間的轉(zhuǎn)換邏輯。下面是NSObject+YYModel講述的主要內(nèi)容:
- 類型編碼的解析
- 數(shù)據(jù)結(jié)構(gòu)的定義
- 遞歸模型的轉(zhuǎn)換
- 接口相關(guān)的代碼
數(shù)據(jù)結(jié)構(gòu)的定義
NSObject+YYModel重新定義了兩個(gè)類,來使用 YYClassInfo 中的封裝。

_YYModelPropertyMeta
/// A property info in object model.
// 對象模型的屬性信息
@interface _YYModelPropertyMeta : NSObject {
@package
// 屬性的名稱
NSString *_name; ///< property's name
// 屬性的類型
YYEncodingType _type; ///< property's type
// 屬性的基礎(chǔ)類型
YYEncodingNSType _nsType; ///< property's Foundation type
// 是否是數(shù)字類型
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
// 如果結(jié)構(gòu)可以用鍵控歸檔器/非歸檔器編碼,則可以
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 的數(shù)組,如果沒有映射多個(gè)鍵的話則返回 nil
NSArray *_mappedToKeyArray; ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys)
// 屬性信息,詳見上文 YYClassPropertyInfo && property_t 章節(jié)
YYClassPropertyInfo *_info; ///< property's info
// 如果有多個(gè)屬性映射到同一個(gè) key 則指向下一個(gè)模型屬性元
_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 的個(gè)數(shù)
NSUInteger _keyMappedCount;
/// Model class type.
// 類型
YYEncodingNSType _nsType;
BOOL _hasCustomWillTransformFromDictionary;
BOOL _hasCustomTransformFromDictionary;
BOOL _hasCustomTransformToDictionary;
BOOL _hasCustomClassFromDictionary;
}
@end

