最簡單的字典
首先,從最簡單的字典開始.
NSDictionary *dict = @{
@"name" : @"Jack",
@"icon" : @"lufy.png",
@"age" : @"20",
@"height" : @1.55,
@"money" : @"100.9",
@"sex" : @(SexFemale),
@"gay" : @"1"
}
目標是拿到字典里的值(value)
對User
模型進行賦值.模型的屬性名
對應字典的鍵(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;
/** 財富 */
@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"];
....
假如屬性數量一多,人工手寫大量樣板代碼
將耗費大量時間和精力,毫無意義.
如果要寫一個框架自動幫我們轉模型出來,大致思路如下:
1.遍歷模型中的屬性
,然后拿到屬性名
作為鍵值
去字典中尋找值
.
2.找到值
后根據模型的屬性
的類型
將值
轉成正確的類型
3.賦值
首先進行第一步:
遍歷模型中的
屬性
,然后拿到屬性名
作為鍵值
去字典中尋找值
.
方法偽代碼:
[模型類 遍歷屬性的方法];
為了方便使用,創建一個叫NSObject+Property
的分類.寫一個獲取所有屬性的方法.
@interface NSObject (Property)
+ (NSArray *)properties;
@end
假設我們看不見一個類的.h
和.m
,有什么辦法可以獲取它所有的實例變量呢?答案是通過運行時機制
.當在實現+ (NSArray *)properties
方法時,需要導入運行時庫.然后使用庫中的API提供的函數得到一個類的方法列表.
注:在舊版本的MJExtension中,獲取成員變量是通過class_copyIvarList來獲取的類的所有實例變量,根據MJ源碼中的說明:"在 swift 中,由于語法結構的變化,使用 Ivar 非常不穩定,經常會崩潰!",所以改用了獲取成員屬性的方法.
另外,不管是獲取成員屬性還是實例變量,都不能獲取到父類的列表.(本人忽略了對父類成員屬性的獲取,后期更新中會更新這一失誤).
// Any instance variables declared by superclasses are not included.
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
返回的是叫objc_property_t
的一個結構體指針,并且通過傳入值引用能夠得到屬性的個數.
#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
來到這里已經獲取到了屬性列表,那么objc_property_t
指向的結構體內部是怎樣的呢.通過搜尋<objc/runtime.h>
頭文件并看不到objc_property_t
的定義的.但好在runtime
開源,我們搜尋到了相關的定義.
typedef struct property_t *objc_property_t;
struct property_t {
const char *name;
const char *attributes;
};
由于知道了結構體的內部構造,就可以獲取內部的成員變量.例如以下方法:
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
在外部調用+ (NSArray *)properties
方法能夠打印出一個類的全部屬性,如:
NSArray *propertyArray = [User properties];
得到控制臺輸出:
從輸出中可以看到該結構體的name
成員表示成員屬性的名字,attributes
表示成員屬性中的一些特性(如是什么類,原子性還是非原子性,是strong還是weak還是copy,生成的成員變量名等信息)...
從蘋果的官方文檔(Objective-C Runtime Programming Guide)可以得知,attributes
是一個類型編碼字符串.可以使用property_getAttributes
函數獲得這個類型編碼字符串.這個字符串以T
作為開始,接上@encode
類型編碼和一個逗號,以V
接上實例變量名作為結尾,在它們之間是一些其他信息,以逗號分割.具體內容可以看官方文檔中詳細的表格.
在實際賦值過程中,我們并不用關心該屬性的內存管理語義,生成的成員變量名,或者其他什么信息.在attributes
中,只需要知道它所屬的類
或者是什么基本數據類型
,即T
至第一個逗號之前
中間的內容,如果是類
的話還需要將@"
和"
去掉.
實際上,框架提供的運行時庫已經給我們提供獲取屬性名
和屬性特性
的函數了.通過下面方式也能打印出相同結果.
NSLog(@"name:%s---attributes:%s",property_getName(property),
property_getAttributes(property));
從runtime
源碼中可以看到這兩個函數的內部是這樣實現的:
const char *property_getName(objc_property_t prop)
{
return prop->name;
}
const char *property_getAttributes(objc_property_t prop)
{
return prop->attributes;
}
再回顧前面說的思路,這時會更清晰:
1.拿到模型的屬性名(注意屬性名和成員變量名的區別),和對應的數據類型.
2.用該屬性名作為鍵去字典中尋找對應的值.
3.拿到值后將值轉換為屬性對應的數據類型.
4.賦值.
現在已經進行到第一步,并且拿到了屬性名
,但是數據類型
還要進一步截取,截取方法如下:
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
// 為了以后方便,將C字符串轉換成OC對象
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);
}
控制臺結果顯示我們能夠截取到其中的類型了.
該部分源碼請看項目實例代碼中的<打印類型>
回歸我們拿到這些數據類型
的初衷,是為了是用字典中的值的類型
與模型中屬性的類型
進行對比,想要對比,需要拿到屬性的類型
,因此需要將這些編碼轉換成一個表示類型的類,創建一個類用來包裝類型.
/**
* 包裝一種類型
*/
@interface MJPropertyType : NSObject
/** 是否為id類型 */
@property (nonatomic, readonly, getter=isIdType) BOOL idType;
/** 是否為基本數字類型:int、float等 */
@property (nonatomic, readonly, getter=isNumberType) BOOL numberType;
/** 是否為BOOL類型 */
@property (nonatomic, readonly, getter=isBoolType) BOOL boolType;
/** 對象類型(如果是基本數據類型,此值為nil) */
@property (nonatomic, readonly) Class typeClass;
@end
OC對象可以通過Class
來表示類型,而基本數據類型只能用布爾來標識.
把這些名字和類型遍歷出來,肯定是為了以后有用,所以需要把它們存起來,由于它們是一個"整體",所以還是設計一個類將他們包裝起來比較好.創建一個包裝成員屬性的類—MJProperty
.
@interface MJProperty : NSObject
/** 成員屬性的名字 */
@property (nonatomic, readonly) NSString *name;
/** 成員屬性的類型 */
@property (nonatomic, readonly) MJPropertyType *type;
@end
這時,代碼就可以進行重構了,將屬于不同類的功能封裝到對應的類上,讓MJProperty
提供一個類方法用于返回一個將objc_property_t
進行包裝的類.
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
MJProperty *propertyObj = [MJProperty propertyWithProperty:property];
}
propertyWithProperty:
方法的實現如下:
+ (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;
}
重構完成之后,結構顯得更加清晰.更有利于接下來的工作.下面繼續完成type
的提取.
該部分源碼請看項目實例代碼中的<重構>
上面獲取到的這些類型,是類型編碼,在蘋果文檔中告訴了我們編碼對應的類型:
根據這個對應關系的圖表,我們將常用的幾個編碼定義成常量字符串或者宏表示它所對應的類型,便于編碼和閱讀:
/**
* 成員變量類型(屬性類型)
*/
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 = @"@";
設置完后,就可以進行提取類型了.
- (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]]);
}
// 是否為數字類型
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;
}
}
}
至此,一個MJProperty
的骨架就大致搭好了.
該部分源碼請看項目實例代碼中的<MJProperty的構建>
當想要使用字典轉模型的功能時,提供一個類方法方便轉換,該方法放在NSObject+keyValue2object
分類中,該分類負責字典轉模型的方法實現.
@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
打印結果:
然后進行下一步----2.用該屬性名作為鍵去字典中尋找對應的值.
id value = [keyValues valueForKey:property.name];
if (!value) continue;
接下來是第三步:3.拿到值后將值的類型轉換為屬性對應的數據類型.
首先處理數字類型,如果模型的屬性是數字類型,即type.isNumberType == YES
.如果字典中的值是字符串類型的,需要將其轉成NSNumber
類型.如果本來就是基本數據類型,則不用進行任何轉換.
if (type.isNumberType){
// 字符串->數字
if ([value isKindOfClass:[NSString class]])
value = [[[NSNumberFormatter alloc]init] numberFromString:value];
}
其中有一種情況,是需要進行特殊處理的.當模型的屬性是char
類型或者bool
類型時,獲取到的編碼都為c
,并且bool
還有可能是B
編碼,它們都對應_boolType
.因為數字類型包含布爾類型,所以bool
類型要在數字類型的條件下進行額外判斷.
if (type.isNumberType){
NSString *oldValue = value;
// 字符串->數字
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;
}
}
}
}
然后處理其他類型轉成字符串類型的情況.
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];
}
}
}
最后,進行賦值.
[self setValue:value forKey:property.name];
最簡單的字典轉模型大致完成了,當然,還有很多細節沒有完善,但細節總是隨著需求的不斷變化而不斷增加的.
該部分源碼請看項目實例代碼中的<簡單的字典轉模型>
JSON字符串 -> 模型
定義一個JSON字符串轉成模型:
/**
* JSON字符串 -> 模型
*/
void keyValues2object1()
{
// 1.定義一個JSON字符串
NSString *jsonString = @"{\"name\":\"Jack\", \"icon\":\"lufy.png\", \"age\":20}";
// 2.將JSON字符串轉為User模型
User *user = [User objectWithKeyValues:jsonString];
// 3.打印User模型的屬性
NSLog(@"name=%@, icon=%@, age=%d", user.name, user.icon, user.age);
}
這時程序會崩潰,因為沒有對程序原來只對字典類型作處理:
// 如果是字符串,到這行就崩了
id value = [keyValues valueForKey:property.name];
所以在這之前需要將JSON轉成Foundation
框架中的對象,蘋果提供了強大的NSJSONSerialization
.利用它,在剛開始傳入字典/JSON字符串的時候將其進行轉換.
- (instancetype)setKeyValues:(id)keyValues{
keyValues = [keyValues JSONObject];
......
}
該方法的具體實現如下,如果是NSString
,就要先轉成NSData
再進行序列化.
- (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;
}
該部分源碼請看項目實例代碼中的<JSON轉模型>
復雜的字典 -> 模型
定義一個模型中包含模型的復雜字典:
NSDictionary *dict = @{
@"text" : @"是啊,今天天氣確實不錯!",
@"user" : @{
@"name" : @"Jack",
@"icon" : @"lufy.png"
},
@"retweetedStatus" : @{
@"text" : @"今天天氣真不錯!",
@"user" : @{
@"name" : @"Rose",
@"icon" : @"nami.png"
}
}
};
對待這種字典的思路,應該想到遞歸,當碰到模型中的屬性類型是一個模型類時,將字典中的值(Value)
作為字典處理.然后再調用字典轉模型的方法返回一個模型類.所以在包裝類型時還要有個屬性表示它是否是自定義的模型類,才能作為依據繼續遞歸.判斷的方法是看它是否是來自于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部分)
下的類結構.
用一個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
的邏輯由類方法實現,在上面的集合中遍歷.由于幾乎所有類都是繼承自NSObject
,所以NSObject
不能寫入上面的集合當中,需要額外判斷:
+ (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;
}
得到結果后,需要在setKeyValues:keyValues
這一核心方法中添加是否為模型類的判斷:
// 如果不是來自foundation框架的類并且不是基本數據類型 ,則遞歸
if (!type.isFromFoundation && typeClass) {
value = [typeClass objectWithKeyValues:value];
}
該部分源碼請看項目實例代碼中的<復雜字典轉模型>
字典數組 -> 模型
稍復雜的一種情況是字典里裝有數組的情況.
NSDictionary *dict = @{
@"statuses" : @[
@{
@"text" : @"今天天氣真不錯!",
@"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"
};
上面定義了一個字典,?模型StatusResult
有兩個數組屬性.
@interface StatusResult : BaseObject
/** 存放著某一頁微博數據(里面都是Status模型) */
@property (strong, nonatomic) NSMutableArray *statuses;
/** 存放著一堆的廣告數據(里面都是Ad模型) */
@property (strong, nonatomic) NSArray *ads;
/** 總數 */
@property (strong, nonatomic) NSNumber *totalNumber;
/** 上一頁的游標 */
@property (assign, nonatomic) long long previousCursor;
/** 下一頁的游標 */
@property (assign, nonatomic) long long nextCursor;
@end
對于一個數組來說,你必須要告訴方法里面裝的是什么模型,才能將字典中值為數組的成員轉成模型.
在MJExtension
中,提供了兩種方式進行處理.
方式一,調用NSObject
分類中得類方法:
[StatusResult setupObjectClassInArray:^NSDictionary *{
return @{
@"statuses" : @"Status",
// 或者 @"statuses" : [Status class],
@"ads" : @"Ad"
// 或者 @"ads" : [Ad class]
};
}];
方式二,在模型的.m
文件中實現方法供回調:
+ (NSDictionary *)objectClassInArray
{
return @{
@"statuses" : @"Status",
// 或者 @"statuses" : [Status class],
@"ads" : @"Ad"
// 或者 @"ads" : [Ad class]
};
}
原理上都差不多,都是通過代碼進行回調,這個主要實現方式二.
在分類中聲明一個protocol
提供接口供模型類調用.
@protocol MJKeyValue <NSObject>
+ (NSDictionary *) objectClassInArray;
@end
在轉換的代碼中設置添加設置數組模型的方法:
if (!type.isFromFoundation && typeClass) {
value = [typeClass objectWithKeyValues:value];
}
// 看該類是否實現了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) {
// 返回一個裝了模型的數組
value = [objectClass objectArrayWithKeyValuesArray:value];
}
}
這時返回的值當然是個裝滿模型的數組模型.思路也很簡單,對數組里的每一個成員都進行字典轉模型的方法.如果其中的成員不是自定義模型類,那么直接返回.
+ (NSMutableArray *)objectArrayWithKeyValuesArray:(id)keyValuesArray{
if ([self isClassFromFoundation:self])
return keyValuesArray;
// 如果是json字符串,轉成字典
keyValuesArray = [keyValuesArray JSONObject];
NSMutableArray *modelArray = [NSMutableArray array];
// 遍歷
for (NSDictionary *keyValues in keyValuesArray) {
// 對其中的模型調用字典轉模型方法,并添加到數組中返回
id model;
model = [self objectWithKeyValues:keyValues];
if (model) {
[modelArray addObject:model];
}
}
return modelArray;
}
該部分源碼請看項目實例代碼中的<字典數組轉模型>
key的替換
@interface IDAndDescription : NSObject
@property (nonatomic, copy) NSString *ID;
@property (nonatomic, copy) NSString *Description;
@end
實際開發中,服務器通常返回一個字段名為id
,或者description
的JSON
數據,而這兩個名字在OC中有?特殊含義,如上所示,在定義屬性的時候并不能使用這類名稱.這時屬性名與字典key不再是直接對應的關系,需要加入一層轉換.
源碼中key的替換也有幾種方式選擇,這里實現replacedKeyFromPropertyName
這一方式.
過程是在要替換key的模型類中實現replacedKeyFromPropertyName
方法,返回一個原始key和更名的key對應的字典.replacedKeyFromPropertyName
在protocol
中聲明.
實際上,也就是創建了一個方法來獲取屬性名與字典key的對應關系.
在模型類中實現接口中的方法告知對應關系.
@implementation IDAndDescription
+ (NSDictionary *)replacedKeyFromPropertyName{
return @{
@"ID" : @"id",
@"Description" : @"description"
};
}
@end
該方法從字典中需找要替換的key,參數是property
的名字.如果字典中找不到對應的屬性名,則不需要進行轉換.
+ (NSString *)propertyKey:(NSString *)propertyName{
NSString *key;
if ([self respondsToSelector:@selector(replacedKeyFromPropertyName)]) {
key = [self replacedKeyFromPropertyName][propertyName];
}
return key?:propertyName;
}
在獲取值(value)
的時候,要將key替換成對應的key.
id value = [keyValues valueForKey:[self.class propertyKey:property.name]];
if (!value) continue;
轉換完成.
性能優化
將5個字典轉模型的例子同時進行運行,在+ properties
方法中添加一句打印.另外之前的例子都是有內存泄露的,這里添加了free(properties)
修復了這個問題.
+ (NSArray *)properties{
NSLog(@"%@調用了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;
}
輸出臺輸出如下:
可以看到,很多的類都不止一次調用了獲取屬性的方法,對于一個類來說,要獲取它的全部屬性,只要獲取一次就夠了.獲取到后將結果緩存起來,下次就不必進行不必要的計算.
注意:由于我寫文章時手上的這份源碼相對較早,緩存屬性列表是通過一個全局字典來緩存的,而在最新版本的
MJExtension
中,已經換成了關聯對象來實現.由于實現思路大致都是一樣,并且效果相同,所以這里并不糾結用哪種方式.
// 設置一個全局字典用來將類的屬性都緩存起來
static NSMutableDictionary *cachedProperties_;
+ (void)load
{
cachedProperties_ = [NSMutableDictionary dictionary];
}
將方法改寫為:
+ (NSArray *)properties
{
NSMutableArray *cachedProperties = cachedProperties_[NSStringFromClass(self)];
if (!cachedProperties) {
NSLog(@"%@調用了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;
}
此時控制臺輸出:
可以看每個類只經過一次獲取全部屬性.
除了緩存屬性外,提取類型編碼的過程也可以進一步緩存優化性能.
在下面的方法中加上一句打印:
- (void)getTypeCode:(NSString *)code
{
NSLog(@"%@",code);
......
}
控制臺輸出:
可以看到一些常用的類型例如NSString
多次調用了該方法.提取類型時,只要知道類名(在這里也就是typeCode
),一個MJPropertyType
就已經可以確定了.
重寫了- ?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;
}
輸出結果:
該部分源碼請看項目實例代碼中的<key的替換與性能優化>