利用Runtime动态绑定Model属性

利用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为属性名,valueKCModelProperty对象的NSDictionary储存,方便使用。

获取属性映射的值

方法很简单,将属性名作为key得到属性映射的值在 dictionary 中的位置keyPath,不要问我怎么获得,这就是之前提到的类方法dictionaryKeyPathByPropertyKey的作用。

注意:如果属性是自定义类型,只需要满足实现了之前定义的KCModelAutoBinding协议,那么就可以通过递归的方式绑定该属性。

使用KVC赋值

以上我们得到了dictionary所在keyPath位置的值,那么怎么把它赋值给属性呢?答案是

Class NSClassFromString(NSString *aClassName);

我们通过这个方法可以得到属性的类,然后就可以开始赋值了。
注意:类分为两种,一种是系统定义好的类,另一种是自定义的类——其他Model对象。因为多数情况下通过解析JSON得到的NSDictionary对象(如使用AFNetworking)里储存的都是系统的类,如:NSIntegerNSArray等,所以如果是第一种类,只要与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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容