利用Runtime动态绑定Model属性
大家如果在开发中使用过从网络获取JSON数据,那么一定对model.value = [dictionary objectForKey:@"key"]
很熟悉,相信大多数刚开始学习iOS网络开发的人都是使用类似以上这句代码将解析为NSDictionary
对象的JSON数据绑定到Model上的。可是如果程序中有很多Model或者Model中有很多属性,这么做就会加大很多工作量,那么有没有什么简单的方法解决这个问题呢?答案就是Runtime技术!
准备工作
首先,建立一个Model类,我把它命名为KCModel
,它是应用中所有Model的父类,应用不能直接使用该类,定义一个协议(面向接口编程),Model实现该协议,我命名为KCModelAutoBinding
,协议声明的方法有:
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary;
+ (NSDictionary *)dictionaryKeyPathByPropertyKey;
- (void)autoBindingWithDictionary:(NSDictionary *)dictionary;
注意其中有两个个类方法,说明如下:
第一个不说了;
+ dictionaryKeyPathByPropertyKey
属性映射的值在dictionary
中的位置,比如myName
属性映射dictionary[@"name"]
,则返回@{@"myName" : @"name"}
,而如果是多层关系,比如映射dictionary[@"data"][@"name"]
,则返回@{@"myName" : @"data.name"}
;
- autoBindingWithDictionary:
将dictionary
绑定到Model。
获取Model所有属性
在Runtime中有个函数可以获取某个类的所有属性:
class_copyPropertyList(Class cls, unsigned int *outCount)
这是一个C语言函数,返回的值是objc_property_t
的指针(代表一个数组)。
需要注意的是这个函数只能获取到当前类的属性,而不能获取到父类的属性,我们可以使用递归的方法获取到包含父类在内的所有属性。
以上我们获得到了objc_property_t
的数组,每个objc_property_t
都代表一个属性,我们可以使用以下方法得到属性名:
property_getName(objc_property_t property)
要想得到更多的信息则需要它了:
property_getAttributes(objc_property_t property)
这个函数返回了一段char
数组字符串给我们,有属性的各种信息,但我们现在只需要一个信息,那就是属性的类型。
来看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
开头,后面跟属性类型等各种信息,信息之间用,
隔开。通过这些我们就可以得到属性的类型了。
我们可以新建一个类来解析并储存属性的这些信息,我把它命名为KCModelProperty
。在KCModel
中,我将所有属性信息用一个key
为属性名,value
为KCModelProperty
对象的NSDictionary
储存,方便使用。
获取属性映射的值
方法很简单,将属性名作为key
得到属性映射的值在 dictionary
中的位置keyPath
,不要问我怎么获得,这就是之前提到的类方法dictionaryKeyPathByPropertyKey
的作用。
注意:如果属性是自定义类型,只需要满足实现了之前定义的KCModelAutoBinding
协议,那么就可以通过递归的方式绑定该属性。
使用KVC赋值
以上我们得到了dictionary
所在keyPath
位置的值,那么怎么把它赋值给属性呢?答案是
Class NSClassFromString(NSString *aClassName);
我们通过这个方法可以得到属性的类,然后就可以开始赋值了。
注意:类分为两种,一种是系统定义好的类,另一种是自定义的类——其他Model对象。因为多数情况下通过解析JSON得到的NSDictionary
对象(如使用AFNetworking
)里储存的都是系统的类,如:NSInteger
、NSArray
等,所以如果是第一种类,只要与dictionary中的值类型一样就可以直接用它来赋值了,但是第二种类就需要使用其他方法赋值了,方法就是最前面提到的类方法modelWithDictionary:
,通过这个方法得到其他Model对象,再进行赋值。
赋值方法就是Key-Value Coding
技术的setValue:forKey:
。
</br>
大功告成。
思路说起来很简单,实际动手又是另外一回事。
</br>
附上我的代码:
//KCModel.h
#import <Foundation/Foundation.h>
//快速定义与类相同名称的协议(数组元素类型标记)
#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内容位置
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) { //非系统自带对象
propertyClass = NSClassFromString(propertyClassName);
}
//转换value
switch (propertyType) {
//基本数字类型
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]]) { //储存KCItem子类对象的数组
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)] //属性为实现了KCModelAutoBinding协议的对象
&& [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
{
//获取缓存数据
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代表数组)
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个属性
const char *propertyCharName = property_getName(property); //获取当前属性的名称
NSString *propertyName = @(propertyCharName);
KCModelProperty *propertyInfo = [[KCModelProperty alloc] initWithPropertyName:propertyName objcProperty:property];
[ret setValue:propertyInfo forKey:propertyName];
}
free(properties);
//设置缓存数据
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:@","]; //属性描述数组
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,且遵循某个协议
static const int arrayPrefixLen = sizeof(arrayPrefix) - 1;
if (typeCharAttr[1] == '\0') {
// string is "@"
_propertyType = KCModelPropertyTypeAny;
} else if (strncmp(typeCharAttr, arrayPrefix, arrayPrefixLen) == 0) {
/*******************
因为只有NSArray遵循某个协议才能被property_getAttributes()函数识别出来,
以此为标记表示这个数组存储着以协议名为类名的Model对象
*******************/
_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