最簡(jiǎn)單的字典
首先,從最簡(jiǎn)單的字典開始.
NSDictionary *dict = @{
@"name" : @"Jack",
@"icon" : @"lufy.png",
@"age" : @"20",
@"height" : @1.55,
@"money" : @"100.9",
@"sex" : @(SexFemale),
@"gay" : @"1"
}
目標(biāo)是拿到字典里的值(value)對(duì)User模型進(jìn)行賦值.模型的屬性名對(duì)應(yīng)字典的鍵(key).
typedef enum {
SexMale,
SexFemale
} Sex;
@interface User : NSObject
/** 名稱 */
@property (copy, nonatomic) NSString *name;
/** 頭像 */
@property (copy, nonatomic) NSString *icon;
/** 年齡 */
@property (assign, nonatomic) unsigned int age;
/** 身高 */
@property (copy, nonatomic) NSString *height;
/** 財(cái)富 */
@property (strong, nonatomic) NSNumber *money;
/** 性別 */
@property (assign, nonatomic) Sex sex;
/** 同性戀 */
@property (assign, nonatomic, getter=isGay) BOOL gay;
@end
最直接的方法是:
User *user = [[User alloc] init];
user.name = dict[@"name"];
user.icon = dict[@"icon"];
....
假如屬性數(shù)量一多,人工手寫大量樣板代碼將耗費(fèi)大量時(shí)間和精力,毫無意義.
如果要寫一個(gè)框架自動(dòng)幫我們轉(zhuǎn)模型出來,大致思路如下:
1.遍歷模型中的屬性,然后拿到屬性名作為鍵值去字典中尋找值.
2.找到值后根據(jù)模型的屬性的類型將值轉(zhuǎn)成正確的類型
3.賦值
首先進(jìn)行第一步:
遍歷模型中的
屬性,然后拿到屬性名作為鍵值去字典中尋找值.
方法偽代碼:
[模型類 遍歷屬性的方法];
為了方便使用,創(chuàng)建一個(gè)叫NSObject+Property的分類.寫一個(gè)獲取所有屬性的方法.
@interface NSObject (Property)
+ (NSArray *)properties;
@end
假設(shè)我們看不見一個(gè)類的.h和.m,有什么辦法可以獲取它所有的實(shí)例變量呢?答案是通過運(yùn)行時(shí)機(jī)制.當(dāng)在實(shí)現(xiàn)+ (NSArray *)properties方法時(shí),需要導(dǎo)入運(yùn)行時(shí)庫(kù).然后使用庫(kù)中的API提供的函數(shù)得到一個(gè)類的方法列表.
注:在舊版本的MJExtension中,獲取成員變量是通過class_copyIvarList來獲取的類的所有實(shí)例變量,根據(jù)MJ源碼中的說明:"在 swift 中,由于語法結(jié)構(gòu)的變化,使用 Ivar 非常不穩(wěn)定,經(jīng)常會(huì)崩潰!",所以改用了獲取成員屬性的方法.
另外,不管是獲取成員屬性還是實(shí)例變量,都不能獲取到父類的列表.(本人忽略了對(duì)父類成員屬性的獲取,后期更新中會(huì)更新這一失誤).
// Any instance variables declared by superclasses are not included.
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
返回的是叫objc_property_t的一個(gè)結(jié)構(gòu)體指針,并且通過傳入值引用能夠得到屬性的個(gè)數(shù).
#import "NSObject+Property.h"
#import <objc/runtime.h>
@implementation NSObject (Property)
+ (NSArray *)properties{
NSArray *propertiesArray = [NSMutableArray array];
// 1.獲得所有的屬性
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(self, &outCount);
// .....
return propertiesArray;
}
@end
來到這里已經(jīng)獲取到了屬性列表,那么objc_property_t指向的結(jié)構(gòu)體內(nèi)部是怎樣的呢.通過搜尋<objc/runtime.h>頭文件并看不到objc_property_t的定義的.但好在runtime開源,我們搜尋到了相關(guān)的定義.
typedef struct property_t *objc_property_t;
struct property_t {
const char *name;
const char *attributes;
};
由于知道了結(jié)構(gòu)體的內(nèi)部構(gòu)造,就可以獲取內(nèi)部的成員變量.例如以下方法:
typedef struct property_t {
const char *name;
const char *attributes;
} *propertyStruct;
@implementation NSObject (Property)
+ (NSArray *)properties{
NSArray *propertiesArray = [NSMutableArray array];
// 1.獲得所有的屬性
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(self, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSLog(@"name:%s---attributes:%s",((propertyStruct)property)->name,((propertyStruct)property)->attributes);
}
return propertiesArray;
}
@end
在外部調(diào)用+ (NSArray *)properties方法能夠打印出一個(gè)類的全部屬性,如:
NSArray *propertyArray = [User properties];
得到控制臺(tái)輸出:

從輸出中可以看到該結(jié)構(gòu)體的name成員表示成員屬性的名字,attributes表示成員屬性中的一些特性(如是什么類,原子性還是非原子性,是strong還是weak還是copy,生成的成員變量名等信息)...
從蘋果的官方文檔(Objective-C Runtime Programming Guide)可以得知,attributes是一個(gè)類型編碼字符串.可以使用property_getAttributes函數(shù)獲得這個(gè)類型編碼字符串.這個(gè)字符串以T作為開始,接上@encode類型編碼和一個(gè)逗號(hào),以V接上實(shí)例變量名作為結(jié)尾,在它們之間是一些其他信息,以逗號(hào)分割.具體內(nèi)容可以看官方文檔中詳細(xì)的表格.
在實(shí)際賦值過程中,我們并不用關(guān)心該屬性的內(nèi)存管理語義,生成的成員變量名,或者其他什么信息.在attributes中,只需要知道它所屬的類或者是什么基本數(shù)據(jù)類型,即T至第一個(gè)逗號(hào)之前中間的內(nèi)容,如果是類的話還需要將@"和"去掉.
實(shí)際上,框架提供的運(yùn)行時(shí)庫(kù)已經(jīng)給我們提供獲取屬性名和屬性特性的函數(shù)了.通過下面方式也能打印出相同結(jié)果.
NSLog(@"name:%s---attributes:%s",property_getName(property),
property_getAttributes(property));
從runtime源碼中可以看到這兩個(gè)函數(shù)的內(nèi)部是這樣實(shí)現(xiàn)的:
const char *property_getName(objc_property_t prop)
{
return prop->name;
}
const char *property_getAttributes(objc_property_t prop)
{
return prop->attributes;
}
再回顧前面說的思路,這時(shí)會(huì)更清晰:
1.拿到模型的屬性名(注意屬性名和成員變量名的區(qū)別),和對(duì)應(yīng)的數(shù)據(jù)類型.
2.用該屬性名作為鍵去字典中尋找對(duì)應(yīng)的值.
3.拿到值后將值轉(zhuǎn)換為屬性對(duì)應(yīng)的數(shù)據(jù)類型.
4.賦值.
現(xiàn)在已經(jīng)進(jìn)行到第一步,并且拿到了屬性名,但是數(shù)據(jù)類型還要進(jìn)一步截取,截取方法如下:
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
// 為了以后方便,將C字符串轉(zhuǎn)換成OC對(duì)象
NSString *name = @(property_getName(property));
NSString *attributes = @(property_getAttributes(property));
NSUInteger loc = 1;
NSUInteger len = [attributes rangeOfString:@","].location - loc;
NSString *type = [attributes substringWithRange:NSMakeRange(loc, len)];
NSLog(@"%@",type);
}
控制臺(tái)結(jié)果顯示我們能夠截取到其中的類型了.

該部分源碼請(qǐng)看項(xiàng)目實(shí)例代碼中的<打印類型>
回歸我們拿到這些數(shù)據(jù)類型的初衷,是為了是用字典中的值的類型與模型中屬性的類型進(jìn)行對(duì)比,想要對(duì)比,需要拿到屬性的類型,因此需要將這些編碼轉(zhuǎn)換成一個(gè)表示類型的類,創(chuàng)建一個(gè)類用來包裝類型.
/**
* 包裝一種類型
*/
@interface MJPropertyType : NSObject
/** 是否為id類型 */
@property (nonatomic, readonly, getter=isIdType) BOOL idType;
/** 是否為基本數(shù)字類型:int、float等 */
@property (nonatomic, readonly, getter=isNumberType) BOOL numberType;
/** 是否為BOOL類型 */
@property (nonatomic, readonly, getter=isBoolType) BOOL boolType;
/** 對(duì)象類型(如果是基本數(shù)據(jù)類型,此值為nil) */
@property (nonatomic, readonly) Class typeClass;
@end
OC對(duì)象可以通過Class來表示類型,而基本數(shù)據(jù)類型只能用布爾來標(biāo)識(shí).
把這些名字和類型遍歷出來,肯定是為了以后有用,所以需要把它們存起來,由于它們是一個(gè)"整體",所以還是設(shè)計(jì)一個(gè)類將他們包裝起來比較好.創(chuàng)建一個(gè)包裝成員屬性的類—MJProperty.
@interface MJProperty : NSObject
/** 成員屬性的名字 */
@property (nonatomic, readonly) NSString *name;
/** 成員屬性的類型 */
@property (nonatomic, readonly) MJPropertyType *type;
@end
這時(shí),代碼就可以進(jìn)行重構(gòu)了,將屬于不同類的功能封裝到對(duì)應(yīng)的類上,讓MJProperty提供一個(gè)類方法用于返回一個(gè)將objc_property_t進(jìn)行包裝的類.
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
MJProperty *propertyObj = [MJProperty propertyWithProperty:property];
}
propertyWithProperty:方法的實(shí)現(xiàn)如下:
+ (instancetype)propertyWithProperty:(objc_property_t)property{
return [[MJProperty alloc] initWithProperty:property];
}
- (instancetype)initWithProperty:(objc_property_t)property{
if (self = [super init]) {
_name = @(property_getName(property));
_type = [MJPropertyType propertyTypeWithAttributeString:@(property_getAttributes(property))];;
}
return self;
}
MJPropertyType也提供類方法用于包裝類型:
+ (instancetype)propertyTypeWithAttributeString:(NSString *)string{
return [[MJPropertyType alloc] initWithTypeString:string];
}
- (instancetype)initWithTypeString:(NSString *)string
{
if (self = [super init])
{
NSUInteger loc = 1;
NSUInteger len = [string rangeOfString:@","].location - loc;
NSString *type = [string substringWithRange:NSMakeRange(loc, len)];
NSLog(@"%@",type);
}
return self;
}
重構(gòu)完成之后,結(jié)構(gòu)顯得更加清晰.更有利于接下來的工作.下面繼續(xù)完成type的提取.
該部分源碼請(qǐng)看項(xiàng)目實(shí)例代碼中的<重構(gòu)>

上面獲取到的這些類型,是類型編碼,在蘋果文檔中告訴了我們編碼對(duì)應(yīng)的類型:

根據(jù)這個(gè)對(duì)應(yīng)關(guān)系的圖表,我們將常用的幾個(gè)編碼定義成常量字符串或者宏表示它所對(duì)應(yīng)的類型,便于編碼和閱讀:
/**
* 成員變量類型(屬性類型)
*/
NSString *const MJPropertyTypeInt = @"i";
NSString *const MJPropertyTypeShort = @"s";
NSString *const MJPropertyTypeFloat = @"f";
NSString *const MJPropertyTypeDouble = @"d";
NSString *const MJPropertyTypeLong = @"q";
NSString *const MJPropertyTypeChar = @"c";
NSString *const MJPropertyTypeBOOL1 = @"c";
NSString *const MJPropertyTypeBOOL2 = @"b";
NSString *const MJPropertyTypePointer = @"*";
NSString *const MJPropertyTypeIvar = @"^{objc_ivar=}";
NSString *const MJPropertyTypeMethod = @"^{objc_method=}";
NSString *const MJPropertyTypeBlock = @"@?";
NSString *const MJPropertyTypeClass = @"#";
NSString *const MJPropertyTypeSEL = @":";
NSString *const MJPropertyTypeId = @"@";
設(shè)置完后,就可以進(jìn)行提取類型了.
- (instancetype)initWithTypeString:(NSString *)string
{
if (self = [super init])
{
NSUInteger loc = 1;
NSUInteger len = [string rangeOfString:@","].location - loc;
NSString *typeCode = [string substringWithRange:NSMakeRange(loc, len)];
[self getTypeCode:typeCode];
NSLog(@"%@",typeCode);
}
return self;
}
- (void)getTypeCode:(NSString *)code
{
if ([code isEqualToString:MJPropertyTypeId]) {
_idType = YES;
} else if (code.length > 3 && [code hasPrefix:@"@\""]) {
// 去掉@"和",截取中間的類型名稱
_code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
_typeClass = NSClassFromString(_code);
_numberType = (_typeClass == [NSNumber class] || [_typeClass isSubclassOfClass:[NSNumber class]]);
}
// 是否為數(shù)字類型
NSString *lowerCode = _code.lowercaseString;
NSArray *numberTypes = @[MJPropertyTypeInt, MJPropertyTypeShort, MJPropertyTypeBOOL1, MJPropertyTypeBOOL2, MJPropertyTypeFloat, MJPropertyTypeDouble, MJPropertyTypeLong, MJPropertyTypeChar];
if ([numberTypes containsObject:lowerCode]) {
_numberType = YES;
if ([lowerCode isEqualToString:MJPropertyTypeBOOL1]
|| [lowerCode isEqualToString:MJPropertyTypeBOOL2]) {
_boolType = YES;
}
}
}
至此,一個(gè)MJProperty的骨架就大致搭好了.
該部分源碼請(qǐng)看項(xiàng)目實(shí)例代碼中的<MJProperty的構(gòu)建>

當(dāng)想要使用字典轉(zhuǎn)模型的功能時(shí),提供一個(gè)類方法方便轉(zhuǎn)換,該方法放在NSObject+keyValue2object分類中,該分類負(fù)責(zé)字典轉(zhuǎn)模型的方法實(shí)現(xiàn).
@implementation NSObject (keyValue2object)
+ (instancetype)objectWithKeyValues:(id)keyValues{
if (!keyValues) return nil;
return [[[self alloc] init] setKeyValues:keyValues];
}
- (instancetype)setKeyValues:(id)keyValues{
NSArray *propertiesArray = [self.class properties];
for (MJProperty *property in propertiesArray) {
MJPropertyType *type = property.type;
Class typeClass = type.typeClass;
if (type.isBoolType) {
NSLog(@"bool");
}else if (type.isIdType){
NSLog(@"ID");
}else if (type.isNumberType){
NSLog(@"Number");
}else{
NSLog(@"%@",typeClass);
}
}
return self;
}
@end
打印結(jié)果:

然后進(jìn)行下一步----2.用該屬性名作為鍵去字典中尋找對(duì)應(yīng)的值.
id value = [keyValues valueForKey:property.name];
if (!value) continue;
接下來是第三步:3.拿到值后將值的類型轉(zhuǎn)換為屬性對(duì)應(yīng)的數(shù)據(jù)類型.
首先處理數(shù)字類型,如果模型的屬性是數(shù)字類型,即type.isNumberType == YES.如果字典中的值是字符串類型的,需要將其轉(zhuǎn)成NSNumber類型.如果本來就是基本數(shù)據(jù)類型,則不用進(jìn)行任何轉(zhuǎn)換.
if (type.isNumberType){
// 字符串->數(shù)字
if ([value isKindOfClass:[NSString class]])
value = [[[NSNumberFormatter alloc]init] numberFromString:value];
}
其中有一種情況,是需要進(jìn)行特殊處理的.當(dāng)模型的屬性是char類型或者bool類型時(shí),獲取到的編碼都為c,并且bool還有可能是B編碼,它們都對(duì)應(yīng)_boolType.因?yàn)閿?shù)字類型包含布爾類型,所以bool類型要在數(shù)字類型的條件下進(jìn)行額外判斷.
if (type.isNumberType){
NSString *oldValue = value;
// 字符串->數(shù)字
if ([value isKindOfClass:[NSString class]]){
value = [[[NSNumberFormatter alloc] init] numberFromString:value];
if (type.isBoolType) {
NSString *lower = [oldValue lowercaseString];
if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"] ) {
value = @YES;
} else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) {
value = @NO;
}
}
}
}
然后處理其他類型轉(zhuǎn)成字符串類型的情況.
else{
if (typeClass == [NSString class]) {
if ([value isKindOfClass:[NSNumber class]]) {
if (type.isNumberType)
// NSNumber -> NSString
value = [value description];
}else if ([value isKindOfClass:[NSURL class]]){
// NSURL -> NSString
value = [value absoluteString];
}
}
}
最后,進(jìn)行賦值.
[self setValue:value forKey:property.name];
最簡(jiǎn)單的字典轉(zhuǎn)模型大致完成了,當(dāng)然,還有很多細(xì)節(jié)沒有完善,但細(xì)節(jié)總是隨著需求的不斷變化而不斷增加的.
該部分源碼請(qǐng)看項(xiàng)目實(shí)例代碼中的<簡(jiǎn)單的字典轉(zhuǎn)模型>
JSON字符串 -> 模型
定義一個(gè)JSON字符串轉(zhuǎn)成模型:
/**
* JSON字符串 -> 模型
*/
void keyValues2object1()
{
// 1.定義一個(gè)JSON字符串
NSString *jsonString = @"{\"name\":\"Jack\", \"icon\":\"lufy.png\", \"age\":20}";
// 2.將JSON字符串轉(zhuǎn)為User模型
User *user = [User objectWithKeyValues:jsonString];
// 3.打印User模型的屬性
NSLog(@"name=%@, icon=%@, age=%d", user.name, user.icon, user.age);
}
這時(shí)程序會(huì)崩潰,因?yàn)闆]有對(duì)程序原來只對(duì)字典類型作處理:
// 如果是字符串,到這行就崩了
id value = [keyValues valueForKey:property.name];
所以在這之前需要將JSON轉(zhuǎn)成Foundation框架中的對(duì)象,蘋果提供了強(qiáng)大的NSJSONSerialization.利用它,在剛開始傳入字典/JSON字符串的時(shí)候?qū)⑵溥M(jìn)行轉(zhuǎn)換.
- (instancetype)setKeyValues:(id)keyValues{
keyValues = [keyValues JSONObject];
......
}
該方法的具體實(shí)現(xiàn)如下,如果是NSString,就要先轉(zhuǎn)成NSData再進(jìn)行序列化.
- (id)JSONObject{
id foundationObj;
if ([self isKindOfClass:[NSString class]]) {
foundationObj = [NSJSONSerialization JSONObjectWithData:[(NSString *)self dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
}else if ([self isKindOfClass:[NSData class]]){
foundationObj = [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
}
return foundationObj?:self;
}
該部分源碼請(qǐng)看項(xiàng)目實(shí)例代碼中的<JSON轉(zhuǎn)模型>
復(fù)雜的字典 -> 模型
定義一個(gè)模型中包含模型的復(fù)雜字典:
NSDictionary *dict = @{
@"text" : @"是啊,今天天氣確實(shí)不錯(cuò)!",
@"user" : @{
@"name" : @"Jack",
@"icon" : @"lufy.png"
},
@"retweetedStatus" : @{
@"text" : @"今天天氣真不錯(cuò)!",
@"user" : @{
@"name" : @"Rose",
@"icon" : @"nami.png"
}
}
};
對(duì)待這種字典的思路,應(yīng)該想到遞歸,當(dāng)碰到模型中的屬性類型是一個(gè)模型類時(shí),將字典中的值(Value)作為字典處理.然后再調(diào)用字典轉(zhuǎn)模型的方法返回一個(gè)模型類.所以在包裝類型時(shí)還要有個(gè)屬性表示它是否是自定義的模型類,才能作為依據(jù)繼續(xù)遞歸.判斷的方法是看它是否是來自于Foundation框架的類.
/** 類型是否來自于Foundation框架,比如NSString、NSArray */
@property (nonatomic, readonly, getter = isFromFoundation) BOOL fromFoundation;
在提取類型的方法中添加這樣一條:
else if (code.length > 3 && [code hasPrefix:@"@\""]) {
// 去掉@"和",截取中間的類型名稱
_code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
_typeClass = NSClassFromString(_code);
_numberType = (_typeClass == [NSNumber class] || [_typeClass isSubclassOfClass:[NSNumber class]]);
// 判斷是否是模型類
_fromFoundation = [NSObject isClassFromFoundation:_typeClass];
}
怎么判斷是否來自Foundation框架呢? 下圖展示了Foundation框架(NSObject部分)下的類結(jié)構(gòu).

用一個(gè)NSSet(比用NSArray檢索效率更高),返回一些常用基本的Foundation框架下繼承自NSObject的類.
static NSSet *foundationClasses_;
+ (NSSet *)foundationClasses
{
if (foundationClasses_ == nil) {
foundationClasses_ = [NSSet setWithObjects:
[NSURL class],
[NSDate class],
[NSValue class],
[NSData class],
[NSArray class],
[NSDictionary class],
[NSString class],
[NSAttributedString class], nil];
}
return foundationClasses_;
}
具體isClassFromFoundation的邏輯由類方法實(shí)現(xiàn),在上面的集合中遍歷.由于幾乎所有類都是繼承自NSObject,所以NSObject不能寫入上面的集合當(dāng)中,需要額外判斷:
+ (BOOL)isClassFromFoundation:(Class)c{
if (c == [NSObject class]) return YES;
__block BOOL result = NO;
[[self foundationClasses] enumerateObjectsUsingBlock:^(Class foundationClass, BOOL *stop) {
if ([c isSubclassOfClass:foundationClass]) {
result = YES;
*stop = YES;
}
}];
return result;
}
得到結(jié)果后,需要在setKeyValues:keyValues這一核心方法中添加是否為模型類的判斷:
// 如果不是來自foundation框架的類并且不是基本數(shù)據(jù)類型 ,則遞歸
if (!type.isFromFoundation && typeClass) {
value = [typeClass objectWithKeyValues:value];
}
該部分源碼請(qǐng)看項(xiàng)目實(shí)例代碼中的<復(fù)雜字典轉(zhuǎn)模型>
字典數(shù)組 -> 模型
稍復(fù)雜的一種情況是字典里裝有數(shù)組的情況.
NSDictionary *dict = @{
@"statuses" : @[
@{
@"text" : @"今天天氣真不錯(cuò)!",
@"user" : @{
@"name" : @"Rose",
@"icon" : @"nami.png"
}
},
@{
@"text" : @"明天去旅游了",
@"user" : @{
@"name" : @"Jack",
@"icon" : @"lufy.png"
}
}
],
@"ads" : @[
@{
@"image" : @"ad01.png",
@"url" : @"http://www.小碼哥ad01.com"
},
@{
@"image" : @"ad02.png",
@"url" : @"http://www.小碼哥ad02.com"
}
],
@"totalNumber" : @"2014",
@"previousCursor" : @"13476589",
@"nextCursor" : @"13476599"
};
上面定義了一個(gè)字典,?模型StatusResult有兩個(gè)數(shù)組屬性.
@interface StatusResult : BaseObject
/** 存放著某一頁(yè)微博數(shù)據(jù)(里面都是Status模型) */
@property (strong, nonatomic) NSMutableArray *statuses;
/** 存放著一堆的廣告數(shù)據(jù)(里面都是Ad模型) */
@property (strong, nonatomic) NSArray *ads;
/** 總數(shù) */
@property (strong, nonatomic) NSNumber *totalNumber;
/** 上一頁(yè)的游標(biāo) */
@property (assign, nonatomic) long long previousCursor;
/** 下一頁(yè)的游標(biāo) */
@property (assign, nonatomic) long long nextCursor;
@end
對(duì)于一個(gè)數(shù)組來說,你必須要告訴方法里面裝的是什么模型,才能將字典中值為數(shù)組的成員轉(zhuǎn)成模型.
在MJExtension中,提供了兩種方式進(jìn)行處理.
方式一,調(diào)用NSObject分類中得類方法:
[StatusResult setupObjectClassInArray:^NSDictionary *{
return @{
@"statuses" : @"Status",
// 或者 @"statuses" : [Status class],
@"ads" : @"Ad"
// 或者 @"ads" : [Ad class]
};
}];
方式二,在模型的.m文件中實(shí)現(xiàn)方法供回調(diào):
+ (NSDictionary *)objectClassInArray
{
return @{
@"statuses" : @"Status",
// 或者 @"statuses" : [Status class],
@"ads" : @"Ad"
// 或者 @"ads" : [Ad class]
};
}
原理上都差不多,都是通過代碼進(jìn)行回調(diào),這個(gè)主要實(shí)現(xiàn)方式二.
在分類中聲明一個(gè)protocol提供接口供模型類調(diào)用.
@protocol MJKeyValue <NSObject>
+ (NSDictionary *) objectClassInArray;
@end
在轉(zhuǎn)換的代碼中設(shè)置添加設(shè)置數(shù)組模型的方法:
if (!type.isFromFoundation && typeClass) {
value = [typeClass objectWithKeyValues:value];
}
// 看該類是否實(shí)現(xiàn)了objectClassInArray方法
else if ([self.class respondsToSelector:@selector(objectClassInArray)]){
id objectClass;
// 如果是class類型,例如@"statuses" : [Status class]
objectClass = [self.class objectClassInArray][property.name];
// 如果是NSString類型,例如@"statuses" : @"Status"
if ([objectClass isKindOfClass:[NSString class]]) {
objectClass = NSClassFromString(objectClass);
}
// 如果有值
if (objectClass) {
// 返回一個(gè)裝了模型的數(shù)組
value = [objectClass objectArrayWithKeyValuesArray:value];
}
}
這時(shí)返回的值當(dāng)然是個(gè)裝滿模型的數(shù)組模型.思路也很簡(jiǎn)單,對(duì)數(shù)組里的每一個(gè)成員都進(jìn)行字典轉(zhuǎn)模型的方法.如果其中的成員不是自定義模型類,那么直接返回.
+ (NSMutableArray *)objectArrayWithKeyValuesArray:(id)keyValuesArray{
if ([self isClassFromFoundation:self])
return keyValuesArray;
// 如果是json字符串,轉(zhuǎn)成字典
keyValuesArray = [keyValuesArray JSONObject];
NSMutableArray *modelArray = [NSMutableArray array];
// 遍歷
for (NSDictionary *keyValues in keyValuesArray) {
// 對(duì)其中的模型調(diào)用字典轉(zhuǎn)模型方法,并添加到數(shù)組中返回
id model;
model = [self objectWithKeyValues:keyValues];
if (model) {
[modelArray addObject:model];
}
}
return modelArray;
}
該部分源碼請(qǐng)看項(xiàng)目實(shí)例代碼中的<字典數(shù)組轉(zhuǎn)模型>
key的替換
@interface IDAndDescription : NSObject
@property (nonatomic, copy) NSString *ID;
@property (nonatomic, copy) NSString *Description;
@end
實(shí)際開發(fā)中,服務(wù)器通常返回一個(gè)字段名為id,或者description的JSON數(shù)據(jù),而這兩個(gè)名字在OC中有?特殊含義,如上所示,在定義屬性的時(shí)候并不能使用這類名稱.這時(shí)屬性名與字典key不再是直接對(duì)應(yīng)的關(guān)系,需要加入一層轉(zhuǎn)換.
源碼中key的替換也有幾種方式選擇,這里實(shí)現(xiàn)replacedKeyFromPropertyName這一方式.
過程是在要替換key的模型類中實(shí)現(xiàn)replacedKeyFromPropertyName方法,返回一個(gè)原始key和更名的key對(duì)應(yīng)的字典.replacedKeyFromPropertyName在protocol中聲明.
實(shí)際上,也就是創(chuàng)建了一個(gè)方法來獲取屬性名與字典key的對(duì)應(yīng)關(guān)系.
在模型類中實(shí)現(xiàn)接口中的方法告知對(duì)應(yīng)關(guān)系.
@implementation IDAndDescription
+ (NSDictionary *)replacedKeyFromPropertyName{
return @{
@"ID" : @"id",
@"Description" : @"description"
};
}
@end
該方法從字典中需找要替換的key,參數(shù)是property的名字.如果字典中找不到對(duì)應(yīng)的屬性名,則不需要進(jìn)行轉(zhuǎn)換.
+ (NSString *)propertyKey:(NSString *)propertyName{
NSString *key;
if ([self respondsToSelector:@selector(replacedKeyFromPropertyName)]) {
key = [self replacedKeyFromPropertyName][propertyName];
}
return key?:propertyName;
}
在獲取值(value)的時(shí)候,要將key替換成對(duì)應(yīng)的key.
id value = [keyValues valueForKey:[self.class propertyKey:property.name]];
if (!value) continue;
轉(zhuǎn)換完成.
性能優(yōu)化
將5個(gè)字典轉(zhuǎn)模型的例子同時(shí)進(jìn)行運(yùn)行,在+ properties方法中添加一句打印.另外之前的例子都是有內(nèi)存泄露的,這里添加了free(properties)修復(fù)了這個(gè)問題.
+ (NSArray *)properties{
NSLog(@"%@調(diào)用了properties方法",[self class]);
NSMutableArray *propertiesArray = [NSMutableArray array];
// 1.獲得所有的屬性
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(self, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
MJProperty *propertyObj = [MJProperty propertyWithProperty:property];
[propertiesArray addObject:propertyObj];
}
free(properties);
return propertiesArray;
}
輸出臺(tái)輸出如下:

可以看到,很多的類都不止一次調(diào)用了獲取屬性的方法,對(duì)于一個(gè)類來說,要獲取它的全部屬性,只要獲取一次就夠了.獲取到后將結(jié)果緩存起來,下次就不必進(jìn)行不必要的計(jì)算.
注意:由于我寫文章時(shí)手上的這份源碼相對(duì)較早,緩存屬性列表是通過一個(gè)全局字典來緩存的,而在最新版本的
MJExtension中,已經(jīng)換成了關(guān)聯(lián)對(duì)象來實(shí)現(xiàn).由于實(shí)現(xiàn)思路大致都是一樣,并且效果相同,所以這里并不糾結(jié)用哪種方式.
// 設(shè)置一個(gè)全局字典用來將類的屬性都緩存起來
static NSMutableDictionary *cachedProperties_;
+ (void)load
{
cachedProperties_ = [NSMutableDictionary dictionary];
}
將方法改寫為:
+ (NSArray *)properties
{
NSMutableArray *cachedProperties = cachedProperties_[NSStringFromClass(self)];
if (!cachedProperties) {
NSLog(@"%@調(diào)用了properties方法",[self class]);
cachedProperties = [NSMutableArray array];
// 1.獲得所有的屬性
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(self, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
MJProperty *propertyObj = [MJProperty propertyWithProperty:property];
[cachedProperties addObject:propertyObj];
}
free(properties);
cachedProperties_[NSStringFromClass(self)] = cachedProperties;
}
return cachedProperties;
}
此時(shí)控制臺(tái)輸出:

可以看每個(gè)類只經(jīng)過一次獲取全部屬性.
除了緩存屬性外,提取類型編碼的過程也可以進(jìn)一步緩存優(yōu)化性能.
在下面的方法中加上一句打印:
- (void)getTypeCode:(NSString *)code
{
NSLog(@"%@",code);
......
}
控制臺(tái)輸出:

可以看到一些常用的類型例如NSString多次調(diào)用了該方法.提取類型時(shí),只要知道類名(在這里也就是typeCode),一個(gè)MJPropertyType就已經(jīng)可以確定了.
重寫了- ?initWithTypeString:方法:
static NSMutableDictionary *cachedTypes_;
+ (void)load
{
cachedTypes_ = [NSMutableDictionary dictionary];
}
+ (instancetype)propertyTypeWithAttributeString:(NSString *)string{
return [[MJPropertyType alloc] initWithTypeString:string];
}
- (instancetype)initWithTypeString:(NSString *)string
{
NSUInteger loc = 1;
NSUInteger len = [string rangeOfString:@","].location - loc;
NSString *typeCode = [string substringWithRange:NSMakeRange(loc, len)];
if (!cachedTypes_[typeCode])
{
NSLog(@"%@",typeCode);
self = [super init];
[self getTypeCode:typeCode];
cachedTypes_[typeCode] = self;
}
return self;
}
輸出結(jié)果:

該部分源碼請(qǐng)看項(xiàng)目實(shí)例代碼中的<key的替換與性能優(yōu)化>