YYModel庫中涉及到Runtime、CF API、訊號和鎖、位的操作。學習該庫可以學習到使用Runtime獲取類的資訊,包括:類屬性資訊、類ivar資訊、類方法、型別編碼;使用runtime底層技術進行方法呼叫,也就是objc_msgSend方法的使用;dispatch_semaphore_t訊號鎖的使用;CF框架中CFMutableDictionaryRef/CFMutableDictionaryRef物件的操作;位的操作。
- 簡單使用介紹
- 簡單的轉化
- 自定義屬性和json欄位的對映配置
- 黑名單和白名單配置
- 型別對映配置
- 預備知識
- Type Encodings
- Property Type String
- 程式碼解析
- 類資訊的獲取
- 配置資訊的獲取
- 模型物件的轉換
- 其它重要知識點
- 位操作
- 訊號和鎖
- CF框架API
YYModel使用介紹
首先會介紹下YYModel的使用,作為下面程式碼解析
章節的鋪墊,程式碼解析
章節以使用方式作為入口點切入,研究框架整體的實現思路、步驟以及每個步驟使用的詳細技術。
簡單的轉化
類屬性的定義和json資料中的key是一致的,這種情況最為簡單,不用配置對映關係,使用NSObject+YYModel
的方法yy_modelWithDictionary
獲取yy_modelWithJSON
即可以把資料反序列化為物件。
比如元素的JSON資料如下所示:
{
"id": 7975492,
"title": "同學們,開學了,準備好早起了嗎?\n",
"source_text": "來自新浪微博",
"share_count": 1,
"praise_num": 999,
"comment_num": 0,
"is_praise": 0
}
複製程式碼
定義的資料模型類如下:
@interface IMYOriginalRecommendWeiboModel : IMYRecommendBaseModel <YYModel>
@property (nonatomic, copy) NSString *source_text;
@property (nonatomic, assign) NSInteger share_count;
@property (nonatomic, assign) NSInteger praise_num; ///< 點贊數
@property (nonatomic, assign) BOOL is_praise; ///< 是否已點贊
@property (nonatomic, assign) NSInteger comment_num; ///< 評論數量
@end
複製程式碼
轉換後的結果如下:

自定義屬性和json欄位的對映配置
多數情況下,服務端的介面是為了多平臺開發的,不同平臺的屬性定義標準、格式不一樣,多數情況下我們需要把服務端的資料個數對映為平臺適應的格式,這種情況需要用到配置自定義屬性和json欄位的對映,可以重寫YYModel
協議的方法modelCustomPropertyMapper
返回一個配置。
定義的資料模型類如下:
@interface IMYOriginalRecommendWeiboModel : IMYRecommendBaseModel <YYModel>
@property (nonatomic, copy) NSString *sourceText;
@property (nonatomic, assign) NSInteger shareCount;
@property (nonatomic, assign) NSInteger praiseCount; ///< 點贊數
@property (nonatomic, assign) BOOL isPraise; ///< 是否已點贊
@property (nonatomic, assign) NSInteger commentCount; ///< 評論數量
@end
複製程式碼
對映關係配置如下:
@implementation IMYRecommendWeiboModel
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"sourceText" : @"source_text",
@"shareCount" : @"share_count",
@"praiseCount" : @"praise_num",
@"isPraise" : @"is_praise",
@"commentCount" : @"comment_num",
};
}
@end
複製程式碼
轉換後的結果如下:

黑名單和白名單配置
黑名單配置如下:
@implementation IMYRecommendBlackListWeiboModel
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"sourceText" : @"source_text",
@"shareCount" : @"share_count",
@"praiseCount" : @"praise_num",
@"isPraise" : @"is_praise",
@"commentCount" : @"comment_num",
};
}
+ (NSArray<NSString *> *)modelPropertyBlacklist {
return @[@"sourceText", @"shareCount"];
}
@end
複製程式碼
反序列化的結果,配置在黑名單中的屬性(sourceText
和shareCount
)不會被賦值

白名單配置如下:
@implementation IMYRecommendWhiteListWeiboModel
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"sourceText" : @"source_text",
@"shareCount" : @"share_count",
@"praiseCount" : @"praise_num",
@"isPraise" : @"is_praise",
@"commentCount" : @"comment_num",
};
}
+ (NSArray<NSString *> *)modelPropertyWhitelist {
return @[@"sourceText", @"shareCount"];
}
@end
複製程式碼
反序列化的結果,只有配置在白名單中的屬性(sourceText
和shareCount
)才會被賦值

型別對映配置
型別對映配置用於屬性是陣列型別,需要配置該屬性中元素的類型別,比如我們定義的資料模型如下,有個users
指定是陣列型別
@interface IMYRecommendExtendWeiboModel : IMYRecommendBaseModel <YYModel>
@property (nonatomic, copy) NSString *sourceText;
@property (nonatomic, assign) NSInteger shareCount;
@property (nonatomic, assign) NSInteger praiseCount; ///< 點贊數
@property (nonatomic, assign) BOOL isPraise; ///< 是否已點贊
@property (nonatomic, assign) NSInteger commentCount; ///< 評論數量
@property (nonatomic, strong) NSArray<IMYUser *> *users;
@end
複製程式碼
型別對映配置如下:
@implementation IMYRecommendBlackListWeiboModel
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"sourceText" : @"source_text",
@"shareCount" : @"share_count",
@"praiseCount" : @"praise_num",
@"isPraise" : @"is_praise",
@"commentCount" : @"comment_num",
};
}
+ (NSDictionary *)modelContainerPropertyGenericClass {
return @{@"users" : [IMYUser class]};
}
@end
複製程式碼
反序列化的結果如下:

預備知識
首先需要了解的是蘋果對於型別說明的的一些規範,包括了Type Encodings和Property Type String,YYModel程式碼中重要的一部分就是對這些資訊進行建模處理。
Type Encodings
Type Encodings 指的是屬性、引數、變數的型別,比如基本資料型別有char、int、short、long、等;此外還有物件型別、類型別、陣列型別、結構體型別、共用體型別、OC中的SEL型別等。其中Block 型別的比較特殊,該型別的的型別定義為:"@?",在YYModel中處理Block型別的程式碼如下:
case '@': {
if (len == 2 && *(type + 1) == '?')
return YYEncodingTypeBlock | qualifier;
else
return YYEncodingTypeObject | qualifier;
}
複製程式碼
Property Type String
Property Type String 指的是property的記憶體屬性、讀寫屬性、原子屬性、getter/getter屬性、屬性對應的ivar名稱以及屬性本身的Type Encoding,其中property的記憶體屬性、讀寫屬性、原子屬性是隻有屬性名稱,沒有屬性值,而property的getter/getter屬性、屬性對應的ivar名稱以及屬性本身的Type Encoding除了屬性名稱之外還會附加一個屬性的值。檢視property的屬性名字和值可以使用property_getAttributes
方法獲取,返回的是一個const char *
型別,如果需要對屬性的名稱和值進行詳細的分析可以使用property_copyAttributeList
獲取到一個陣列,陣列的元素是objc_property_attribute_t
這種結構體型別的,已經解析好了name
和value
,方便使用。
假設定義有如下的屬性@property(getter=isIntReadOnlyGetter, readonly) int intReadonlyGetter;
,使用property_getAttributes
方法獲取到的屬性對應的屬性描述如下:Ti,R,GisIntReadOnlyGetter
,intReadonlyGetter
屬性對應的objc_property_attribute_t
型別為p
,基本的規則解釋如下:
- 型別 p.name == 'T',需要獲取 p.value值,然後查詢
Type Encodings
表格 - 對應的ivar名稱 p.name == 'V',需要獲取 p.value值,p.value值為ivar名稱
- 讀寫屬性 p.name == 'R' -> readonly
- 記憶體屬性 p.name == 'C' -> copy; p.name == '&' -> retain; p.name == 'W' -> weak
- 原子屬性 p.name == 'N' -> nonatomic
- getter/setter屬性 p.name = 'G' -> G後面的內容為自定義的getter方法名稱,使用p.value獲取;p.name = 'S' -> S後面的內容為自定義的setter方法名稱,使用p.value獲取
以下是一段使用property_getAttributes
方法獲取屬性名字和值得示例程式碼
定義一個測試的類包含有如下幾個屬性
@interface MyObj : NSObject
@property (nonatomic, copy) MyBlock block;
@property struct YorkshireTeaStruct structDefault;
@property(nonatomic, readonly, retain) id idReadonlyRetainNonatomic;
@property(getter=isIntReadOnlyGetter, readonly) int intReadonlyGetter;
@end
複製程式碼
使用如下的程式碼解析property的屬性
char *propertyAttributes = (char *)property_getAttributes(property);
NSString *propertyAttributesString = [NSString stringWithUTF8String:propertyAttributes];
NSLog(@"propertyAttributesString = %@", propertyAttributesString);
複製程式碼
使用以上的程式碼對MyObj
類的屬性進行分析,列印的內容如下,可以對照Property Type String和Type Encodings檢視詳細的說明:
propertyAttributesString = T@?,C,N,V_block
propertyAttributesString = T{YorkshireTeaStruct=ic},V_structDefault
propertyAttributesString = T@,R,&,N,V_idReadonlyRetainNonatomic
propertyAttributesString = Ti,R,GisIntReadOnlyGetter,V_intReadonlyGetter
複製程式碼
程式碼解析
類資訊的獲取
類資訊的獲取主要是在YYClassInfo
類的方法_update
中處理的,包括類的類物件、父類的類物件、父類的類資訊、是否是元類、屬性資訊、方法資訊、ivar資訊。實際上方法資訊、ivar資訊這兩個資訊並沒有真正的使用到,但是框架中有進行處理,為了完整性還是稍作敘述。主要使用到的還是propertyInfos
屬性中的內容,後面的配置資訊獲取和模型物件轉換都需要使用到。
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<NSString *, YYClassIvarInfo *> *ivarInfos; ///< ivars
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< methods
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; ///< properties
// ...
複製程式碼
流程時序圖如下所示:

獲取類資訊的操作主要都在_update
方法中,主要步驟如下
- class_copyMethodList 方法獲取類中的方法資訊,方法資訊轉換為YYClassMethodInfo物件,儲存在methodInfos屬性中
- class_copyPropertyList 方法獲取類中的屬性資訊,屬性資訊轉換為YYClassPropertyInfo物件,儲存在propertyInfos屬性中
- class_copyIvarList 方法獲取類中的ivar資訊,ivar資訊轉換為YYClassIvarInfo物件,儲存在ivarInfos屬性中
這裡需要注意的是class_copyMethodList
、class_copyPropertyList
、class_copyIvarList
涉及到記憶體的拷貝問題,使用完成之後需要使用C的方法free釋放拷貝的內容,防止記憶體洩漏。上面有提到說方法資訊、ivar資訊這兩個資訊並沒有真正的使用到,接下來會著重的介紹屬性資訊的獲取中的一些細節。
- (void)_update {
_ivarInfos = nil;
_methodInfos = nil;
_propertyInfos = nil;
Class cls = self.cls;
// 獲取類中的方法資訊
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(cls, &methodCount);
if (methods) {
NSMutableDictionary *methodInfos = [NSMutableDictionary new];
_methodInfos = methodInfos;
for (unsigned int i = 0; i < methodCount; i++) {
YYClassMethodInfo *info = [[YYClassMethodInfo alloc] initWithMethod:methods[i]];
if (info.name) methodInfos[info.name] = info;
}
free(methods);
}
// 獲取類中的屬性資訊
unsigned int propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
if (properties) {
NSMutableDictionary *propertyInfos = [NSMutableDictionary new];
_propertyInfos = propertyInfos;
for (unsigned int i = 0; i < propertyCount; i++) {
YYClassPropertyInfo *info = [[YYClassPropertyInfo alloc] initWithProperty:properties[i]];
if (info.name) propertyInfos[info.name] = info;
}
free(properties);
}
// 獲取類中的ivar資訊
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList(cls, &ivarCount);
if (ivars) {
NSMutableDictionary *ivarInfos = [NSMutableDictionary new];
_ivarInfos = ivarInfos;
for (unsigned int i = 0; i < ivarCount; i++) {
YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]];
if (info.name) ivarInfos[info.name] = info;
}
free(ivars);
}
if (!_ivarInfos) _ivarInfos = @{};
if (!_methodInfos) _methodInfos = @{};
if (!_propertyInfos) _propertyInfos = @{};
_needUpdate = NO;
}
複製程式碼
屬性資訊獲取
屬性資訊主要包含屬性名字、屬性的Encoding 型別、屬性所屬的類、屬性的getter/setter方法的選擇子SEL。這些資訊會儲存在屬性資訊類YYClassPropertyInfo
中,屬性資訊類YYClassPropertyInfo
定義如下:
@interface YYClassPropertyInfo : NSObject
@property (nonatomic, assign, readonly) objc_property_t property; ///< property's opaque struct
@property (nonatomic, strong, readonly) NSString *name; ///< property's name
@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
@property (nullable, nonatomic, assign, readonly) Class cls; ///< may be nil
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *protocols; ///< may nil
@property (nonatomic, assign, readonly) SEL getter; ///< getter (nonnull)
@property (nonatomic, assign, readonly) SEL setter; ///< setter (nonnull)
// ...
複製程式碼
屬性處理主要在initWithProperty
方法中進行,這部分的內容在前面的預備知識中的Type Encodings和Property Type String有講到了大部分,主要步驟如下:
- 獲取property名字
- 讀取property的屬性名字和屬性值,建立屬性的Encoding模型
- 處理getter/setter
- (instancetype)initWithProperty:(objc_property_t)property {
if (!property) return nil;
self = [super init];
_property = property;
// 獲取property名字
const char *name = property_getName(property);
if (name) {
_name = [NSString stringWithUTF8String:name];
}
// 讀取property的屬性名字和屬性值,建立屬性的Encoding模型
YYEncodingType type = 0;
unsigned int attrCount;
objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
for (unsigned int i = 0; i < attrCount; i++) {
switch (attrs[i].name[0]) {
case 'T': { // Type encoding
if (attrs[i].value) {
_typeEncoding = [NSString stringWithUTF8String:attrs[i].value];
type = YYEncodingGetType(attrs[i].value);
if ((type & YYEncodingTypeMask) == YYEncodingTypeObject && _typeEncoding.length) {
NSScanner *scanner = [NSScanner scannerWithString:_typeEncoding];
if (![scanner scanString:@"@\"" intoString:NULL]) continue;
NSString *clsName = nil;
if ([scanner scanUpToCharactersFromSet: [NSCharacterSet characterSetWithCharactersInString:@"\"<"] intoString:&clsName]) {
if (clsName.length) _cls = objc_getClass(clsName.UTF8String);
}
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;
}
}
} break;
case 'V': { // Instance variable
if (attrs[i].value) {
_ivarName = [NSString stringWithUTF8String:attrs[i].value];
}
} break;
case 'R': {
type |= YYEncodingTypePropertyReadonly;
} break;
case 'C': {
type |= YYEncodingTypePropertyCopy;
} break;
case '&': {
type |= YYEncodingTypePropertyRetain;
} break;
case 'N': {
type |= YYEncodingTypePropertyNonatomic;
} break;
case 'D': {
type |= YYEncodingTypePropertyDynamic;
} break;
case 'W': {
type |= YYEncodingTypePropertyWeak;
} break;
case 'G': {
type |= YYEncodingTypePropertyCustomGetter;
if (attrs[i].value) {
_getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
}
} break;
case 'S': {
type |= YYEncodingTypePropertyCustomSetter;
if (attrs[i].value) {
_setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
}
} // break; commented for code coverage in next line
default: break;
}
}
if (attrs) {
free(attrs);
attrs = NULL;
}
_type = type;
// 處理getter、setter方法
if (_name.length) {
if (!_getter) {
_getter = NSSelectorFromString(_name);
}
if (!_setter) {
_setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]);
}
}
return self;
}
複製程式碼
配置資訊的獲取
配置資訊的獲取主要流程時序圖如下:

主要有以下幾個步驟:
- 黑名單配置獲取
- 白名單配置獲取
- 容器型別中元素型別配置獲取
- 遞迴遍歷當前類和父類的propertyInfos屬性,獲取所有property的後設資料
- 處理自定義的屬性對於json資料key的對映配置
對應的程式碼如下,關鍵的地方有新增了註釋:
- (instancetype)initWithClass:(Class)cls {
YYClassInfo *classInfo = [YYClassInfo classInfoWithClass:cls];
if (!classInfo) return nil;
self = [super init];
// 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];
}
}
// 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;
}
}
// 遞迴遍歷當前類和父類的propertyInfos屬性,獲取所有property的後設資料
// property的後設資料對應的私有類為_YYModelPropertyMeta,其實就是YYClassPropertyInfo類物件的封裝
NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
YYClassInfo *curClassInfo = classInfo;
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]];
if (!meta || !meta->_name) continue;
if (!meta->_getter || !meta->_setter) continue;
if (allPropertyMetas[meta->_name]) continue;
allPropertyMetas[meta->_name] = meta;
}
curClassInfo = curClassInfo.superClassInfo;
}
if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy;
// create mapper
// 處理自定義的屬性對於json資料key的對映配置
NSMutableDictionary *mapper = [NSMutableDictionary new];
NSMutableArray *keyPathPropertyMetas = [NSMutableArray new];
NSMutableArray *multiKeysPropertyMetas = [NSMutableArray new];
if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
NSDictionary *customMapper = [(id <YYModel>)cls modelCustomPropertyMapper];
[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];
}
// 處理有多個屬性繫結到同一個json資料key的配置
propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;
} else if ([mappedToKey isKindOfClass:[NSArray class]]) {
NSMutableArray *mappedToKeyArray = [NSMutableArray new];
for (NSString *oneKey in ((NSArray *)mappedToKey)) {
if (![oneKey isKindOfClass:[NSString class]]) continue;
if (oneKey.length == 0) continue;
NSArray *keyPath = [oneKey componentsSeparatedByString:@"."];
if (keyPath.count > 1) {
[mappedToKeyArray addObject:keyPath];
} else {
[mappedToKeyArray addObject:oneKey];
}
if (!propertyMeta->_mappedToKey) {
propertyMeta->_mappedToKey = oneKey;
propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil;
}
}
if (!propertyMeta->_mappedToKey) return;
propertyMeta->_mappedToKeyArray = mappedToKeyArray;
[multiKeysPropertyMetas addObject:propertyMeta];
// 處理有多個屬性繫結到同一個json資料key的配置
propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;
}
}];
}
// 處理預設的屬性的對映配置,屬性在json資料中的key就是屬性的原始名稱
[allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
propertyMeta->_mappedToKey = name;
propertyMeta->_next = mapper[name] ?: nil;
mapper[name] = propertyMeta;
}];
// 最終把json資料key和屬性的對映關係儲存起來
if (mapper.count) _mapper = mapper;
if (keyPathPropertyMetas) _keyPathPropertyMetas = keyPathPropertyMetas;
if (multiKeysPropertyMetas) _multiKeysPropertyMetas = multiKeysPropertyMetas;
_classInfo = classInfo;
_keyMappedCount = _allPropertyMetas.count;
_nsType = YYClassGetNSType(cls);
_hasCustomWillTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomWillTransformFromDictionary:)]);
_hasCustomTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformFromDictionary:)]);
_hasCustomTransformToDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformToDictionary:)]);
_hasCustomClassFromDictionary = ([cls respondsToSelector:@selector(modelCustomClassForDictionary:)]);
return self;
}
複製程式碼
模型物件的轉換
模型物件的轉換的步驟時序圖如下:
入口函式yy_modelSetWithDictionary
的功能如下:
- 1、屬性的對映配置的個數大於json資料元素的個數(if分支),優先處理json資料
- 1.1、遍歷json資料的key
- 1.2、根據key從
meta->_mapper
配置中尋找對映的_YYModelPropertyMeta
物件 - 1.3、從json資料中讀取值,給屬性設定值。
- 2、json資料元素的個數大於屬性的對映配置的個數(else分支),優先處理屬性屬性
- 2.1、從
modelMeta->_allPropertyMetas
讀取屬性配置 - 2.2、從json資料中讀取值,給屬性設定值
- 2.1、從
對應的程式碼如下,關鍵的地方有新增了註釋:
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
if (!dic || dic == (id)kCFNull) return NO;
if (![dic isKindOfClass:[NSDictionary class]]) return NO;
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
if (modelMeta->_keyMappedCount == 0) return NO;
if (modelMeta->_hasCustomWillTransformFromDictionary) {
dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
if (![dic isKindOfClass:[NSDictionary class]]) return NO;
}
ModelSetContext context = {0};
context.modelMeta = (__bridge void *)(modelMeta);
context.model = (__bridge void *)(self);
context.dictionary = (__bridge void *)(dic);
// 這裡做這個處理主要是為了優化效能,哪個少優先處理哪個,提高效率
// 1、屬性的對映配置的個數大於json資料元素的個數(if分支),優先處理json資料
// 1.1、遍歷json資料的key
// 1.2、根據key從`meta->_mapper`配置中尋找對映的`_YYModelPropertyMeta`物件
// 1.3、從json資料中讀取值,給屬性設定值。
// * 因為`meta->_mapper`儲存的是一對一的對映關係,所以另外需要額外的處理keypath對映和陣列型別的key的對映
// 2、json資料元素的個數大於屬性的對映配置的個數(else分支),優先處理屬性屬性
// 2.1、從`modelMeta->_allPropertyMetas`讀取屬性配置
// 2.2、從json資料中讀取值,給屬性設定值
if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
// 處理多個屬性對映到同一個json資料的key
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
if (modelMeta->_keyPathPropertyMetas) {
// 處理keypath,屬性對應json資料key是多層型別的對映
CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
if (modelMeta->_multiKeysPropertyMetas) {
// 處理陣列型別對映,屬性對應json資料key是多個的對映
CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
} else {
// 處理屬性多json資料的key的一對一對映,這種情況多個屬性對映到同一個json資料的key處理方式和ModelSetWithDictionaryFunction方法的不一樣,不過最終的結果是一樣的
CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
CFRangeMake(0, modelMeta->_keyMappedCount),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
if (modelMeta->_hasCustomTransformFromDictionary) {
return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
}
return YES;
}
複製程式碼
以上程式碼中使用到的兩個方法ModelSetWithDictionaryFunction
、ModelSetWithPropertyMetaArrayFunction
,這兩個方法很簡單,使用獲取值方法YYValueForMultiKeys
、YYValueForKeyPath
,根據_YYModelPropertyMeta
物件的配置從json資料中獲取值,然後呼叫ModelSetValueForProperty
給物件的屬性設定值。
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;
};
}
static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) {
ModelSetContext *context = _context;
__unsafe_unretained NSDictionary *dictionary = (__bridge NSDictionary *)(context->dictionary);
__unsafe_unretained _YYModelPropertyMeta *propertyMeta = (__bridge _YYModelPropertyMeta *)(_propertyMeta);
if (!propertyMeta->_setter) return;
id value = nil;
if (propertyMeta->_mappedToKeyArray) {
value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray);
} else if (propertyMeta->_mappedToKeyPath) {
value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath);
} else {
value = [dictionary objectForKey:propertyMeta->_mappedToKey];
}
if (value) {
__unsafe_unretained id model = (__bridge id)(context->model);
ModelSetValueForProperty(model, value, propertyMeta);
}
}
複製程式碼
獲取值方法YYValueForMultiKeys
、YYValueForKeyPath
也比較簡單
YYValueForMultiKeys
深度遍歷json資料取值YYValueForKeyPath
廣度遍歷json資料取值
static force_inline id YYValueForKeyPath(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *keyPaths) {
id value = nil;
for (NSUInteger i = 0, max = keyPaths.count; i < max; i++) {
value = dic[keyPaths[i]];
if (i + 1 < max) {
if ([value isKindOfClass:[NSDictionary class]]) {
dic = value;
} else {
return nil;
}
}
}
return value;
}
static force_inline id YYValueForMultiKeys(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *multiKeys) {
id value = nil;
for (NSString *key in multiKeys) {
if ([key isKindOfClass:[NSString class]]) {
value = dic[key];
if (value) break;
} else {
value = YYValueForKeyPath(dic, (NSArray *)key);
if (value) break;
}
}
return value;
}
複製程式碼
到了最關鍵的設定屬性值的方法ModelSetValueForProperty
,使用底層的C語言方法objc_msgSend
給屬性賦值,該方法區分型別處理值
- 數值型別
- NSFoundation型別,包含了NSArray、NSString、NSDictionary、NSData等型別以及對應的可變型別(如果存在)
- 其餘特殊型別,包含了SEL、block、Struct、Union、C指標、字串指標等
需要注意:
- 1、可變型別需要特殊處理,需要使用mutableCopy複製一個可變物件賦值給屬性,否則使用可變型別的特有api比如addObject就會出現崩潰;
- 2、json資料取出的值和property定義的值型別可能不一樣,如果是數值型別,需要把數值轉換為對應型別,否則編譯不過;如果是NSFoundation型別,需要把Json資料轉換為property定義的型別,否則型別會有問題,方法
objc_msgSend
可以設定成功,比如property中定義的是屬性status_desc是NSString型別,方法objc_msgSend
方法的引數傳遞的是一個NSDate型別,使用status_desc的方法比如length就會崩潰,因為型別變為NSDate了,在NSDate中找不到length方法
方法的部分如下:
static void ModelSetValueForProperty(__unsafe_unretained id model,
__unsafe_unretained id value,
__unsafe_unretained _YYModelPropertyMeta *meta) {
if (meta->_isCNumber) {
NSNumber *num = YYNSNumberCreateFromID(value);
ModelSetNumberToProperty(model, num, meta);
if (num) [num class]; // hold the number
} else if (meta->_nsType) {
if (value == (id)kCFNull) {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
} else {
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);
}
} else if ([value isKindOfClass:[NSNumber class]]) {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
meta->_setter,
(meta->_nsType == YYEncodingTypeNSString) ?
((NSNumber *)value).stringValue :
((NSNumber *)value).stringValue.mutableCopy);
} else if ([value isKindOfClass:[NSData class]]) {
NSMutableString *string = [[NSMutableString alloc] initWithData:value encoding:NSUTF8StringEncoding];
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, string);
} else if ([value isKindOfClass:[NSURL class]]) {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
meta->_setter,
(meta->_nsType == YYEncodingTypeNSString) ?
((NSURL *)value).absoluteString :
((NSURL *)value).absoluteString.mutableCopy);
} else if ([value isKindOfClass:[NSAttributedString class]]) {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
meta->_setter,
(meta->_nsType == YYEncodingTypeNSString) ?
((NSAttributedString *)value).string :
((NSAttributedString *)value).string.mutableCopy);
}
} break;
// ... 省略很多
default: break;
}
}
} else {
BOOL isNull = (value == (id)kCFNull);
switch (meta->_type & YYEncodingTypeMask) {
case YYEncodingTypeObject: {
if (isNull) {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil);
} else if ([value isKindOfClass:meta->_cls] || !meta->_cls) {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)value);
} 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;
case YYEncodingTypeClass: {
if (isNull) {
((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)NULL);
} else {
Class cls = nil;
if ([value isKindOfClass:[NSString class]]) {
cls = NSClassFromString(value);
if (cls) {
((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)cls);
}
} else {
cls = object_getClass(value);
if (cls) {
if (class_isMetaClass(cls)) {
((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)value);
}
}
}
}
} break;
case YYEncodingTypeSEL: {
if (isNull) {
((void (*)(id, SEL, SEL))(void *) objc_msgSend)((id)model, meta->_setter, (SEL)NULL);
} else if ([value isKindOfClass:[NSString class]]) {
SEL sel = NSSelectorFromString(value);
if (sel) ((void (*)(id, SEL, SEL))(void *) objc_msgSend)((id)model, meta->_setter, (SEL)sel);
}
} break;
case YYEncodingTypeBlock: {
if (isNull) {
((void (*)(id, SEL, void (^)()))(void *) objc_msgSend)((id)model, meta->_setter, (void (^)())NULL);
} else if ([value isKindOfClass:YYNSBlockClass()]) {
((void (*)(id, SEL, void (^)()))(void *) objc_msgSend)((id)model, meta->_setter, (void (^)())value);
}
} break;
case YYEncodingTypeStruct:
case YYEncodingTypeUnion:
case YYEncodingTypeCArray: {
if ([value isKindOfClass:[NSValue class]]) {
const char *valueType = ((NSValue *)value).objCType;
const char *metaType = meta->_info.typeEncoding.UTF8String;
if (valueType && metaType && strcmp(valueType, metaType) == 0) {
[model setValue:value forKey:meta->_name];
}
}
} break;
case YYEncodingTypePointer:
case YYEncodingTypeCString: {
if (isNull) {
((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, (void *)NULL);
} else if ([value isKindOfClass:[NSValue class]]) {
NSValue *nsValue = value;
if (nsValue.objCType && strcmp(nsValue.objCType, "^v") == 0) {
((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, nsValue.pointerValue);
}
}
} // break; commented for code coverage in next line
default: break;
}
}
}
複製程式碼
其它重要知識點
位操作
YYModel框架中定義了一個列舉YYEncodingType
,這個型別的值可以儲存property的值型別(Type Encoding);property的讀寫、原子、記憶體等屬性(Property Encoding);方法的型別(Method Encoding)(暫時沒用到)。在獲取型別方法YYEncodingGetType
獲取型別的時候使用位活操作符"|"組合多個Encoding,在設定屬性值的方法比如ModelSetNumberToProperty
,使用位與操作符“&”判斷是否是特定的型別。使用這種型別有兩個好處:1、節省空間;2、位操作效率比較高,比如乘除2,使用左移或者右移的效率會比乘除法高,不過實際編碼中為了可讀性還是最好使用乘除法,另外現代的編譯器會幫我們做很多優化,在絕大多數的場景中,我們可以不用太關注這些細枝末節,真正需要優化的時候才考慮做這類程式碼上的優化。
訊號和鎖
YYModel中使用的是訊號鎖dispatch_semaphore_t
,為什麼使用這種型別的鎖呢,可以從作者的部落格中找到答案不再安全的 OSSpinLock
,這種鎖的效率還是非常高的,又可以避免OSSpinLock鎖產生的問題。這個場景中dispatch_semaphore_t
是一個讀寫互斥鎖,CFDictionaryGetValue是讀操作,CFDictionarySetValue是寫操作,這兩者同一時間只能一個進入,其餘的需要等待。
+ (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;
}
複製程式碼
CF框架API
yy_modelSetWithDictionary
方法中使用到CF框架中容器遍歷的API有CFDictionaryApplyFunction
和CFArrayApplyFunction
,一般滴使用CF框架的API效能會高於NSFoundation框架的API,因為NSFoundation框架的API是基於CF框架的API,此時NSFoundation框架相當於一箇中間者,繞了一步。更多的關於CFArray的知識可以參考Exposing NSMutableArray這篇文章的介紹。