Runtime 동적 귀속 모델 속성 이용

17450 단어
Runtime 동적 귀속 모델 속성 이용
개발 과정 에서 네트워크 에서 JSON 데 이 터 를 가 져 온 적 이 있다 면 model.value = [dictionary objectForKey:@"key"] 에 대해 잘 알 고 있 을 것 입 니 다. iOS 네트워크 개발 을 처음 배 운 사람들 은 대부분 유사 한 코드 를 사용 하여 NSDictionary 대상 으로 분 석 된 JSON 데 이 터 를 모델 에 연결 할 것 이 라 고 믿 습 니 다.그러나 프로그램 에 Model 이나 Model 에 속성 이 많 으 면 작업량 이 많아 집 니 다. 그러면 이 문 제 를 해결 하 는 간단 한 방법 이 있 습 니까?정 답 은 런 타임 기술!
준비 작업
먼저, 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 귀속.
모델 의 모든 속성 가 져 오기
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 런 타임 안내 보기:
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: 으로 이 방법 을 통 해 다른 모델 대상 을 얻어 값 을 부여 하 는 것 이다.할당 방법 은 바로 Key-Value Coding 기술 의 setValue:forKey: 이다.
큰 성 과 를 거두다.
생각 은 말 하기 가 매우 간단 하지만, 실제로 손 을 쓰 는 것 은 또 다른 일이 다.
나의 코드 를 동봉 합 니 다:
//KCModel.h

#import 

//             (        )
#define KC_ARRAY_TYPE(VAL) \
@protocol VAL  \
@end

@protocol KCModelAutoBinding 

+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary;
+ (NSArray *)modelsWithArray:(NSArray *)array;
- (void)autoBindingWithDictionary:(NSDictionary *)dictionary;

@end

@interface KCModel : NSObject 

+ (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 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 
#import 

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= 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 

@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

좋은 웹페이지 즐겨찾기