美文网首页iOS DeveloperiOS学习专题
利用Runtime动态绑定Model属性

利用Runtime动态绑定Model属性

作者: koce_zhao | 来源:发表于2016-08-11 11:05 被阅读484次

    利用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
    

    相关文章

      网友评论

        本文标题:利用Runtime动态绑定Model属性

        本文链接:https://www.haomeiwen.com/subject/sdzjsttx.html