利用Runtime動(dòng)態(tài)綁定Model屬性
大家如果在開發(fā)中使用過從網(wǎng)絡(luò)獲取JSON數(shù)據(jù),那么一定對(duì)model.value = [dictionary objectForKey:@"key"]
很熟悉豁鲤,相信大多數(shù)剛開始學(xué)習(xí)iOS網(wǎng)絡(luò)開發(fā)的人都是使用類似以上這句代碼將解析為NSDictionary
對(duì)象的JSON數(shù)據(jù)綁定到Model上的秽誊。可是如果程序中有很多Model或者M(jìn)odel中有很多屬性琳骡,這么做就會(huì)加大很多工作量锅论,那么有沒有什么簡單的方法解決這個(gè)問題呢?答案就是Runtime技術(shù)楣号!
準(zhǔn)備工作
首先最易,建立一個(gè)Model類,我把它命名為KCModel
竖席,它是應(yīng)用中所有Model的父類耘纱,應(yīng)用不能直接使用該類,定義一個(gè)協(xié)議(面向接口編程)毕荐,Model實(shí)現(xiàn)該協(xié)議束析,我命名為KCModelAutoBinding
,協(xié)議聲明的方法有:
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary;
+ (NSDictionary *)dictionaryKeyPathByPropertyKey;
- (void)autoBindingWithDictionary:(NSDictionary *)dictionary;
注意其中有兩個(gè)個(gè)類方法憎亚,說明如下:
第一個(gè)不說了员寇;
+ dictionaryKeyPathByPropertyKey
屬性映射的值在dictionary
中的位置弄慰,比如myName
屬性映射dictionary[@"name"]
,則返回@{@"myName" : @"name"}
蝶锋,而如果是多層關(guān)系陆爽,比如映射dictionary[@"data"][@"name"]
,則返回@{@"myName" : @"data.name"}
扳缕;
- autoBindingWithDictionary:
將dictionary
綁定到Model慌闭。
獲取Model所有屬性
在Runtime中有個(gè)函數(shù)可以獲取某個(gè)類的所有屬性:
class_copyPropertyList(Class cls, unsigned int *outCount)
這是一個(gè)C語言函數(shù),返回的值是objc_property_t
的指針(代表一個(gè)數(shù)組)躯舔。
需要注意的是這個(gè)函數(shù)只能獲取到當(dāng)前類的屬性驴剔,而不能獲取到父類的屬性,我們可以使用遞歸的方法獲取到包含父類在內(nèi)的所有屬性粥庄。
以上我們獲得到了objc_property_t
的數(shù)組丧失,每個(gè)objc_property_t
都代表一個(gè)屬性,我們可以使用以下方法得到屬性名:
property_getName(objc_property_t property)
要想得到更多的信息則需要它了:
property_getAttributes(objc_property_t property)
這個(gè)函數(shù)返回了一段char
數(shù)組字符串給我們惜互,有屬性的各種信息布讹,但我們現(xiàn)在只需要一個(gè)信息,那就是屬性的類型训堆。
來看Apple的Runtime指南:
You can use the property_getAttributes function to discover the name, the @encode type string of a property, and other attributes of the property.
The string starts with a T followed by the @encode type and a comma, and finishes with a V followed by the name of the backing instance variable.
也就是說描验,返回的字符串是以T
開頭,后面跟屬性類型等各種信息坑鱼,信息之間用,
隔開挠乳。通過這些我們就可以得到屬性的類型了。
我們可以新建一個(gè)類來解析并儲(chǔ)存屬性的這些信息姑躲,我把它命名為KCModelProperty
。在KCModel
中盟蚣,我將所有屬性信息用一個(gè)key
為屬性名黍析,value
為KCModelProperty
對(duì)象的NSDictionary
儲(chǔ)存,方便使用屎开。
獲取屬性映射的值
方法很簡單阐枣,將屬性名作為key
得到屬性映射的值在 dictionary
中的位置keyPath
,不要問我怎么獲得奄抽,這就是之前提到的類方法dictionaryKeyPathByPropertyKey
的作用蔼两。
注意:如果屬性是自定義類型,只需要滿足實(shí)現(xiàn)了之前定義的KCModelAutoBinding
協(xié)議逞度,那么就可以通過遞歸的方式綁定該屬性额划。
使用KVC賦值
以上我們得到了dictionary
所在keyPath
位置的值,那么怎么把它賦值給屬性呢档泽?答案是
Class NSClassFromString(NSString *aClassName);
我們通過這個(gè)方法可以得到屬性的類俊戳,然后就可以開始賦值了揖赴。
注意:類分為兩種,一種是系統(tǒng)定義好的類抑胎,另一種是自定義的類——其他Model對(duì)象燥滑。因?yàn)槎鄶?shù)情況下通過解析JSON得到的NSDictionary
對(duì)象(如使用AFNetworking
)里儲(chǔ)存的都是系統(tǒng)的類,如:NSInteger
阿逃、NSArray
等铭拧,所以如果是第一種類,只要與dictionary中的值類型一樣就可以直接用它來賦值了恃锉,但是第二種類就需要使用其他方法賦值了搀菩,方法就是最前面提到的類方法modelWithDictionary:
,通過這個(gè)方法得到其他Model對(duì)象淡喜,再進(jìn)行賦值秕磷。
賦值方法就是Key-Value Coding
技術(shù)的setValue:forKey:
。
</br>
大功告成炼团。
思路說起來很簡單澎嚣,實(shí)際動(dòng)手又是另外一回事。
</br>
附上我的代碼:
//KCModel.h
#import <Foundation/Foundation.h>
//快速定義與類相同名稱的協(xié)議(數(shù)組元素類型標(biāo)記)
#define KC_ARRAY_TYPE(VAL) \
@protocol VAL <NSObject> \
@end
@protocol KCModelAutoBinding <NSObject>
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary;
+ (NSArray *)modelsWithArray:(NSArray *)array;
- (void)autoBindingWithDictionary:(NSDictionary *)dictionary;
@end
@interface KCModel : NSObject <KCModelAutoBinding>
+ (NSDictionary *)dictionaryKeyPathByPropertyKey;
@end
//KCModel.m
static id KCTransformNormalValueForClass(id val, NSString *className) {
id ret = val;
Class valClass = [val class];
Class cls = nil;
if (className.length > 0) {
cls = NSClassFromString(className);
}
if (!cls || !valClass) {
ret = nil;
} else if (![cls isSubclassOfClass:[val class]] && ![valClass isSubclassOfClass:cls]) {
ret = nil;
}
return ret;
}
@implementation KCModel
#pragma mark -- KCItemAutoBinding
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary
{
id<KCModelAutoBinding> model = [[self class] new];
[model autoBindingWithDictionary:dictionary];
return model;
}
+ (NSArray *)modelsWithArray:(NSArray *)array
{
NSMutableArray *models = @[].mutableCopy;
for (NSDictionary *dict in array) {
[models addObject:[self modelWithDictionary:dict]];
}
return [NSArray arrayWithArray:models];
}
- (void)autoBindingWithDictionary:(NSDictionary *)dictionary
{
NSDictionary *properties = [self.class propertyInfos];
NSDictionary *dictionaryKeyPathByPropertyKey = [self.class dictionaryKeyPathByPropertyKey];
for (KCModelProperty *property in [properties allValues]) {
KCModelPropertyType propertyType = property.propertyType;
NSString *propertyName = property.propertyName;
NSString *propertyClassName = property.propertyClassName;
NSString *propertyKeyPath = propertyName;
//獲取屬性映射的dictionary內(nèi)容位置
if ([dictionaryKeyPathByPropertyKey objectForKey:propertyName]) {
propertyKeyPath = [dictionaryKeyPathByPropertyKey objectForKey:propertyName];
}
id value = [dictionary kc_valueForKeyPath:propertyKeyPath]; //從dictionary中得到映射的值
if (value == nil || value == [NSNull null]) {
continue;
}
Class propertyClass = nil;
if (propertyClassName.length > 0) { //非系統(tǒng)自帶對(duì)象
propertyClass = NSClassFromString(propertyClassName);
}
//轉(zhuǎn)換value
switch (propertyType) {
//基本數(shù)字類型
case KCModelPropertyTypeInt:
case KCModelPropertyTypeFloat:
case KCModelPropertyTypeDouble:
case KCModelPropertyTypeBool:
case KCModelPropertyTypeNumber:{
if ([value isKindOfClass:[NSString class]]) {
NSNumberFormatter *numberFormatter = [NSNumberFormatter new];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
value = [numberFormatter numberFromString:value];
}else{
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSNumber class]));
}
}
break;
case KCModelPropertyTypeChar:{
if ([value isKindOfClass:[NSString class]]) {
char firstCharacter = [value characterAtIndex:0];
value = [NSNumber numberWithChar:firstCharacter];
} else {
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSNumber class]));
}
}
break;
case KCModelPropertyTypeString:{
if ([value isKindOfClass:[NSNumber class]]) {
value = [value stringValue];
} else {
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSString class]));
}
}
break;
case KCModelPropertyTypeData:{
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSData class]));
}
break;
case KCModelPropertyTypeDate:{
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSDate class]));
}
break;
case KCModelPropertyTypeAny:
break;
case KCModelPropertyTypeDictionary:{
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSDictionary class]));
}
break;
case KCModelPropertyTypeMutableDictionary:{
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSDictionary class]));
value = [value mutableCopy];
}
break;
case KCModelPropertyTypeArray:{
if (propertyClass && [propertyClass isSubclassOfClass:[KCModel class]]) { //儲(chǔ)存KCItem子類對(duì)象的數(shù)組
value = [propertyClass itemsWithArray:value];
}else{
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSArray class]));
}
}
break;
case KCModelPropertyTypeMutableArray:{
value = KCTransformNormalValueForClass(value, NSStringFromClass([NSArray class]));
value = [value mutableCopy];
}
break;
case KCModelPropertyTypeObject:
case KCModelPropertyTypeModel:{
if (propertyClass) {
if ([propertyClass conformsToProtocol:@protocol(KCModelAutoBinding)] //屬性為實(shí)現(xiàn)了KCModelAutoBinding協(xié)議的對(duì)象
&& [value isKindOfClass:[NSDictionary class]]) {
NSDictionary *oldValue = value;
value = [[propertyClass alloc] init];
[value autoBindingWithDictionary:oldValue];
}else{
value = KCTransformNormalValueForClass(value, propertyClassName);
}
}
}
break;
}
//KVC
if (value && value != [NSNull null]) {
[self setValue:value forKey:propertyName];
}
}
}
#pragma mark -- Class method
+ (NSDictionary *)propertyInfos
{
//獲取緩存數(shù)據(jù)
NSDictionary *cachedInfos = objc_getAssociatedObject(self, _cmd);
if (cachedInfos != nil) {
return cachedInfos;
}
NSMutableDictionary *ret = [NSMutableDictionary dictionary];
unsigned int propertyCount;
objc_property_t *properties = class_copyPropertyList(self, &propertyCount); //獲取自身的所有屬性(c語言瘟芝,*properties代表數(shù)組)
Class superClass = class_getSuperclass(self);
//獲取父類的所有屬性
if (superClass && ![NSStringFromClass(superClass) isEqualToString:@"KCModel"]) {
NSDictionary *superProperties = [superClass propertyInfos]; //遞歸
[ret addEntriesFromDictionary:superProperties];
}
for (int i = 0; i < propertyCount; i++) {
objc_property_t property = properties[i]; //獲取第i個(gè)屬性
const char *propertyCharName = property_getName(property); //獲取當(dāng)前屬性的名稱
NSString *propertyName = @(propertyCharName);
KCModelProperty *propertyInfo = [[KCModelProperty alloc] initWithPropertyName:propertyName objcProperty:property];
[ret setValue:propertyInfo forKey:propertyName];
}
free(properties);
//設(shè)置緩存數(shù)據(jù)
objc_setAssociatedObject(self, @selector(propertyInfos), ret, OBJC_ASSOCIATION_COPY);
return ret;
}
+ (NSDictionary *)dictionaryKeyPathByPropertyKey
{
return [NSDictionary dictionaryWithObjects:[self propertyNames] forKeys:[self propertyNames]];
}
+ (NSArray *)propertyNames
{
NSDictionary *ret = [self propertyInfos];
return [ret allKeys];
}
@end
//KCModelProperty.h
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
typedef NS_ENUM(NSInteger, KCModelPropertyType) {
KCModelPropertyTypeInt = 0,
KCModelPropertyTypeFloat,
KCModelPropertyTypeDouble,
KCModelPropertyTypeBool,
KCModelPropertyTypeChar,
KCModelPropertyTypeString,
KCModelPropertyTypeNumber,
KCModelPropertyTypeData,
KCModelPropertyTypeDate,
KCModelPropertyTypeAny,
KCModelPropertyTypeArray,
KCModelPropertyTypeMutableArray,
KCModelPropertyTypeDictionary,
KCModelPropertyTypeMutableDictionary,
KCModelPropertyTypeObject,
KCModelPropertyTypeModel
};
@interface KCModelProperty : NSObject
@property (nonatomic, strong, readonly) NSString* propertyClassName;
@property (nonatomic, strong, readonly) NSString* propertyName;
@property (nonatomic, assign, readonly) KCModelPropertyType propertyType;
- (instancetype)initWithPropertyName:(NSString *)propertyName objcProperty:(objc_property_t)objcProperty;
@end
//KCModelProperty.m
#import "KCModelProperty.h"
#import "KCModel.h"
@implementation KCModelProperty
- (instancetype)initWithPropertyName:(NSString *)propertyName objcProperty:(objc_property_t)objcProperty
{
if (self = [super init]) {
_propertyName = propertyName;
/*********************************************
Apple "Objective-C Runtime Programming Guide":
You can use the property_getAttributes function to discover the name,
the @encode type string of a property, and other attributes of the property.
The string starts with a T followed by the @encode type and a comma, and finishes
with a V followed by the name of the backing instance variable.
*********************************************/
const char *attr = property_getAttributes(objcProperty);
NSString *propertyAttributes = @(attr); //使用","隔開的屬性描述字符串
propertyAttributes = [propertyAttributes substringFromIndex:1]; //移除"T"
NSArray *attributes = [propertyAttributes componentsSeparatedByString:@","]; //屬性描述數(shù)組
NSString *typeAttr = attributes[0]; //屬性類型名稱
const char *typeCharAttr = [typeAttr UTF8String];
NSString *encodeCodeStr = [typeAttr substringToIndex:1]; //屬性類型
const char *encodeCode = [encodeCodeStr UTF8String];
const char typeEncoding = *encodeCode;
//判斷類型
switch (typeEncoding) {
case 'i': // int
case 's': // short
case 'l': // long
case 'q': // long long
case 'I': // unsigned int
case 'S': // unsigned short
case 'L': // unsigned long
case 'Q': // unsigned long long
_propertyType = KCModelPropertyTypeInt;
break;
case 'f': // float
_propertyType = KCModelPropertyTypeFloat;
break;
case 'd': // double
_propertyType = KCModelPropertyTypeDouble;
break;
case 'B': // BOOL
_propertyType = KCModelPropertyTypeBool;
break;
case 'c': // char
case 'C': // unsigned char
_propertyType = KCModelPropertyTypeChar;
break;
case '@':{ //object
static const char arrayPrefix[] = "@\"NSArray<"; //NSArray,且遵循某個(gè)協(xié)議
static const int arrayPrefixLen = sizeof(arrayPrefix) - 1;
if (typeCharAttr[1] == '\0') {
// string is "@"
_propertyType = KCModelPropertyTypeAny;
} else if (strncmp(typeCharAttr, arrayPrefix, arrayPrefixLen) == 0) {
/*******************
因?yàn)橹挥蠳SArray遵循某個(gè)協(xié)議才能被property_getAttributes()函數(shù)識(shí)別出來易桃,
以此為標(biāo)記表示這個(gè)數(shù)組存儲(chǔ)著以協(xié)議名為類名的Model對(duì)象
*******************/
_propertyType = KCModelPropertyTypeArray;
NSString *className = [[NSString alloc] initWithBytes:typeCharAttr + arrayPrefixLen
length:strlen(typeCharAttr + arrayPrefixLen) - 2
encoding:NSUTF8StringEncoding];
Class propertyClass = NSClassFromString(className);
if (propertyClass) {
_propertyClassName = NSStringFromClass(propertyClass);
}
} else if (strcmp(typeCharAttr, "@\"NSString\"") == 0) {
_propertyType = KCModelPropertyTypeString;
} else if (strcmp(typeCharAttr, "@\"NSNumber\"") == 0) {
_propertyType = KCModelPropertyTypeNumber;
} else if (strcmp(typeCharAttr, "@\"NSDate\"") == 0) {
_propertyType = KCModelPropertyTypeDate;
} else if (strcmp(typeCharAttr, "@\"NSData\"") == 0) {
_propertyType = KCModelPropertyTypeData;
} else if (strcmp(typeCharAttr, "@\"NSDictionary\"") == 0) {
_propertyType = KCModelPropertyTypeDictionary;
} else if (strcmp(typeCharAttr, "@\"NSArray\"") == 0) {
_propertyType = KCModelPropertyTypeArray;
} else if (strcmp(typeCharAttr, "@\"NSMutableArray\"") == 0){
_propertyType = KCModelPropertyTypeMutableArray;
} else if (strcmp(typeCharAttr, "@\"NSMutableDictionary\"") == 0){
_propertyType = KCModelPropertyTypeMutableDictionary;
}else {
_propertyType = KCModelPropertyTypeObject;
Class propertyClass = nil;
if (typeAttr.length >= 3) {
NSString* className = [typeAttr substringWithRange:NSMakeRange(2, typeAttr.length-3)];
propertyClass = NSClassFromString(className);
}
if (propertyClass) {
if ([propertyClass isSubclassOfClass:[KCModel class]]) {
_propertyType = KCModelPropertyTypeModel;
}
_propertyClassName = NSStringFromClass(propertyClass);
}
}
}
break;
default:
break;
}
}
return self;
}
@end
//NSDictionary+KCModel.h
#import <Foundation/Foundation.h>
@interface NSDictionary (KCModel)
- (id)kc_valueForKeyPath:(NSString *)keyPath;
@end
//NSDictionary+KCModel.m
@implementation NSDictionary (KCModel)
- (id)kc_valueForKeyPath:(NSString *)keyPath
{
NSArray *components = [keyPath componentsSeparatedByString:@"."];
id ret = self;
for (NSString *component in components) {
if (ret == nil || ret == [NSNull null] || ![ret isKindOfClass:[NSDictionary class]]) {
break;
}
ret = ret[component];
}
return ret;
}
@end