Key-Value Coding (KVC)原理

一、kvc基本用法

有如下定义:

typedef struct {
    float x, y, z;
} HPPoint;

@interface HotpotCat : NSObject

@property (nonatomic, copy) NSString *name;

@end

@interface HPObject : NSObject {
    @public
    NSString *nickName;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) double height;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, assign) HPPoint point;
@property (nonatomic, strong) HotpotCat *hotpotCat;

@end

1.1 一般setter以及成员变量访问

HPObject *obj = [HPObject alloc];
obj.name = @"Hotpot";
obj.age = 18;
obj->nickName = @"Cat";

1.2 kvc间接访问

[obj setValue:@"HotpotCat" forKey:@"name"];

1.3 kvc集合类型

1.3.1 数组

对于如下集合类型数据:

obj.array = @[@"1",@"2",@"3"];

修改方式有两种:

//方式一:获取数组,修改数据。重新设置数组
NSArray *array = [obj valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[obj setValue:array forKey:@"array"];
NSLog(@"%@",[obj valueForKey:@"array"]);

//方式二:以可变数组接收,然后修改数据
NSMutableArray *mArray = [obj mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
NSLog(@"%@",[obj valueForKey:@"array"]);
  • 1.通过valueForKey:获取数据后,重新生成新的数组然后setValue: forKey:设置重新设置值。
  • 2.通过mutableArrayValueForKey:获取值,直接修改获取到的可变数组。

1.3.2 字典

NSDictionary *dict = @{
                       @"name":@"Hotpot",
                       @"age":@18,
                       @"height":@180.0
                       };
HPObject *obj = [[HPObject alloc] init];
//字典转模型
[obj setValuesForKeysWithDictionary:dict];
NSLog(@"%@",obj);
//key数组->模型到字典
NSArray *keyArray = @[@"name",@"age",@"height"];
NSDictionary *valueDic = [obj dictionaryWithValuesForKeys:keyArray];
NSLog(@"%@",valueDic);

字典模型互转。

1.3.3 kvc消息传递

NSArray *array = @[@"Hotpot",@"Cat",@"HotpotCat"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
// 消息从array传递给了string
NSLog(@"lenStr:%@",lenStr);
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"lowStr:%@",lowStr);

输出:

lenStr:(
    6,
    3,
    9
)
lowStr:(
    hotpot,
    cat,
    hotpotcat
)
  • length以及lowercaseString消息通过array传递给了string

1.3.4 kvc 聚合操作符

NSMutableArray *dataArray = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
    HPObject *p = [HPObject new];
    NSDictionary* dict = @{
                           @"name":@"Hotpot",
                           @"age":@(18+i),
                           @"height":@(175.0 + 2 * arc4random_uniform(5)),
                           };
    [p setValuesForKeysWithDictionary:dict];
    [dataArray addObject:p];
}

//身高数据
NSLog(@"%@", [dataArray valueForKey:@"height"]);
//平均身高
float avg = [[dataArray valueForKeyPath:@"@avg.height"] floatValue];
NSLog(@"height avg:%f", avg);
//总数
int count = [[dataArray valueForKeyPath:@"@count.height"] intValue];
NSLog(@"height count:%d", count);
//身高和
int sum = [[dataArray valueForKeyPath:@"@sum.height"] intValue];
NSLog(@"height sum:%d", sum);
//最大身高
int max = [[dataArray valueForKeyPath:@"@max.height"] intValue];
NSLog(@"height max:%d", max);
//最小身高
int min = [[dataArray valueForKeyPath:@"@min.height"] intValue];
NSLog(@"height min:%d", min);

输出:

(
    177,
    183,
    185,
    177,
    179
)
height avg:180.199997
height count:5
height sum:901
height max:185
height min:177

可以通过valueForKeyPath配合操作符@avg、@count、@max、@min、@sum对数据进行处理。

1.3.5 kvc 数组操作符

对于1.3.4中的示例代码增加以下逻辑:

NSLog(@"dataArray height:%@", [dataArray valueForKey:@"height"]);
// 返回操作对象指定属性的集合
NSArray *heights = [dataArray valueForKeyPath:@"@unionOfObjects.height"];
NSLog(@"heights:%@", heights);
// 返回操作对象指定属性的集合 -- 去重
NSArray *heightsWithoutDuplicates = [dataArray valueForKeyPath:@"@distinctUnionOfObjects.height"];
NSLog(@"heights:%@", heightsWithoutDuplicates);

输出:

dataArray height:(
    181,
    183,
    177,
    177,
    181
)
heights:(
    181,
    183,
    177,
    177,
    181
)
heights:(
    181,
    177,
    183
)
  • @unionOfObjects返回操作对象的指定属性的集合。
  • @distinctUnionOfObjects返回操作对象指定属性的集合(会进行去重)。

1.3.5.1 数组集合嵌套

NSMutableArray *dataArray = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
    HPObject *p = [HPObject new];
    NSDictionary* dict = @{
                           @"name":@"Hotpot",
                           @"age":@(18 + i),
                           @"height":@(175.0 + 2 * arc4random_uniform(5)),
                           };
    [p setValuesForKeysWithDictionary:dict];
    [dataArray addObject:p];
}

// 嵌套数组
NSArray *newArr = @[dataArray,dataArray];
NSLog(@"newArr height:%@", [newArr valueForKey:@"height"]);
// 返回指定属性的集合
NSArray *heights = [newArr valueForKeyPath:@"@unionOfArrays.height"];
NSLog(@"heights:%@", heights);
// 返回指定属性的集合 -- 去重
NSArray *heightsWithoutDuplicates = [newArr valueForKeyPath:@"@distinctUnionOfArrays.height"];
NSLog(@"heights:%@", heightsWithoutDuplicates);

⚠️当然这里数组中对象可以不同,只要都有height即可。

输出:

newArr height:(
        (
        183,
        181,
        177,
        177,
        179
    ),
        (
        183,
        181,
        177,
        177,
        179
    )
)
heights:(
    183,
    181,
    177,
    177,
    179,
    183,
    181,
    177,
    177,
    179
)
heights:(
    183,
    179,
    181,
    177
)
  • @unionOfArrays将数组中嵌套的集合进行解析,获取指定属性的集合。@ distinctUnionOfArrays会对获取到的属性值进行去重。

1.3.5.2 set集合嵌套

NSMutableSet *dataSet = [NSMutableSet set];
for (int i = 0; i < 5; i++) {
    HPObject *p = [HPObject new];
    NSDictionary* dict = @{
                           @"name":@"Hotpot",
                           @"age":@(18 + i),
                           @"height":@(175.0 + 2 * arc4random_uniform(5)),
                           };
    [p setValuesForKeysWithDictionary:dict];
    [dataSet addObject:p];
}

NSMutableSet *dataSet2 = [NSMutableSet set];
for (int i = 0; i < 5; i++) {
    HPObject *p = [HPObject new];
    NSDictionary* dict = @{
                           @"name":@"Hotpot",
                           @"age":@(18 + i),
                           @"height":@(175.0 + 2 * arc4random_uniform(5)),
                           };
    [p setValuesForKeysWithDictionary:dict];
    [dataSet2 addObject:p];
}


// 嵌套set
NSSet *newSet = [NSSet setWithObjects:dataSet, dataSet2, nil];
NSLog(@"newSet height:%@", [newSet valueForKey:@"height"]);
// 返回指定属性的集合
NSArray *heightsWithoutDuplicates = [newSet valueForKeyPath:@"@distinctUnionOfSets.height"];
NSLog(@"heights:%@", heightsWithoutDuplicates);

输出:

newSet height:{(
        {(
        175,
        181,
        183
    )},
        {(
        175,
        177,
        183,
        179
    )}
)}
heights:{(
    175,
    181,
    177,
    183,
    179
)}
  • @distinctUnionOfSets可以获取嵌套的set中对象的属性集合。

1.4 kvc访问非对象属性

HPPoint point = {1.,2.,3.};
//结构体转换为NSValue
NSValue *value = [NSValue valueWithBytes:&point objCType:@encode(HPPoint)];
HPObject *obj = [HPObject alloc];
[obj setValue:value forKey:@"point"];
NSValue *pointValue = [obj valueForKey:@"point"];
NSLog(@"pointValue:%@",pointValue);
NSLog(@"obj--- x:%f y:%f z:%f",obj.point.x,obj.point.y,obj.point.z);

HPPoint point1;
//NSValue 转换为 结构体
[pointValue getValue:&point1];
NSLog(@"NSValue--- x:%f y:%f z:%f",point1.x,point1.y,point1.z);

输出:

pointValue:{length = 12, bytes = 0x0000803f0000004000004040}
obj--- x:1.000000 y:2.000000 z:3.000000
NSValue--- x:1.000000 y:2.000000 z:3.000000
  • 通过NSValue与结构体互转,存取值。

1.5 kvc 层层访问(keyPath)

HPObject *obj = [HPObject alloc];
obj.name = @"obj";
HotpotCat *hp = [HotpotCat alloc];
hp.name = @"hp";
obj.hotpotCat = hp;
[obj setValue:@"cat" forKeyPath:@"hotpotCat.name"];
NSLog(@"%@",[obj valueForKeyPath:@"hotpotCat.name"]);

二、kvc原理分析

- (void)setValue:(nullable id)value forKey:(NSString *)key;

定义在NSObject(NSKeyValueCoding)分类中(Foundation框架),没有开源。
在苹果的官方文档中有对kvc的详细介绍:
Key-Value Coding Programming Guide

2.1 kvc设值原理

@interface HPObject : NSObject{
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end

Accessor Search Patterns
setValue:forKey:根据官方文档的介绍普通对象逻辑如下:
1. set和_set方法设置值

- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

调用:

HPObject *obj = [HPObject alloc];
[obj setValue:@"HP" forKey:@"name"];
  • 两个都实现先调用setNamesetName没用实现则调用_setName

2. 实例变量设置值 _, _is, ,
先通过accessInstanceVariablesDirectly控制是否开启实例变量访问,默认开启。
既然会通过这4个成员变量进行赋值,那么在都存在的情况下验证下优先级:

HPObject *obj = [HPObject alloc];
[obj setValue:@"Hotpot" forKey:@"name"];
NSLog(@"_name:%@,_isName:%@,name:%@,isName:%@",obj->_name,obj->_isName,obj->name,obj->isName);

输出:

_name:Hotpot,_isName:(null),name:(null),isName:(null)

可以看到实际上只对一个成员变量进行了赋值。那么肯定就有优先级,逐个屏蔽成员变量有以下输出:

_name:Hotpot,_isName:(null),name:(null),isName:(null)
_isName:Hotpot,name:(null),isName:(null)
name:Hotpot,isName:(null)
isName:Hotpot

优先级是_ -> _is -> -> is
⚠️这个时候有个疑问,既然第一步中有set、_set,那是不是也应该有setIs、_setIs
实现setIsName_setIsName

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

//没有调用
- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

1中的验证逻辑相同,setIsName会优先调用,但是_setIsName没有调用。
所以1中的调用优先级是set -> _set -> setIs

3. 以上操作都失败,进入setValue:forUndefinedKey:
如果上面的方法没有实现,实例变量也都没有,则会走到setValue:forUndefinedKey:逻辑,相当于是对于异常可以在这里处理。实现后找不到就会走到这个流程,不会crash了。

2.2 kvc取值原理

1. get, , is, or _ 通过取值方法获取值,如果获取到到跳转第5步,否则继续
实现以下代码:

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

验证:

HPObject *obj = [HPObject alloc];
NSLog(@"getter value:%@",[obj valueForKey:@"name"]);

逐个屏蔽实现的4个方法有如下调用:

getter value:getName
getter value:name
getter value:isName
getter value:_name
  • 调用顺序为:get -> -> is -> _

2. 普通类型方法没有找到,去找countOfobjectInAtIndex:/AtIndexes:
首先要实现countOf,然后实现objectInAtIndex:/AtIndexes:中的一个。此时意味着当前对象拥有一个属性名为NSKeyValueArray类型的属性,它可以响应NSArray的所有方法。一个对象不一定需要显式的写出自己的属性也可以进行存取操作。

//countOf
- (NSInteger)countOfName {
    NSLog(@"%s",__func__);
    return 1;
}

//objectInAtIndex:
- (id)objectInNameAtIndex:(NSInteger)index {
    return @"name1";
}

//AtIndexes:
- (NSArray *)nameAtIndexes:(NSIndexSet *)indexes {
    return @[@"name1"];
}

调用:

HPObject *obj = [HPObject alloc];
NSLog(@"getter value:%@",[obj valueForKey:@"name"]);

输出:

getter value:(
    name1
)
  • 这个时候获取到的值是一个集合,数量由countOf控制。
  • 优先级:objectInAtIndex: - > AtIndexes:

3. 没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找countOf、enumeratorOf、memberOf命名的方法。(属于NSSet

//countOf
- (NSInteger)countOfName {
    NSLog(@"%s",__func__);
    return 5;
}

//enumeratorOf
- (NSEnumerator *)enumeratorOfName {
    NSLog(@"%s",__func__);
     NSSet *set = [NSSet setWithArray:@[@"name1",@"name2",@"name3"]];
    NSEnumerator *enumerator = [set objectEnumerator];
    return enumerator;
}
//memberOf:
- (id)memberOfName:(id)obj {
    NSLog(@"%s",__func__);
    return @"name1";
}

如果找到这三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。否则,继续执行第4步。

⚠️这块代码调试有问题。

4. 实例变量取值 _, _is, ,
accessInstanceVariablesDirectly返回YES

HPObject *obj = [HPObject alloc];
obj->_name = @"_name";
obj->_isName = @"_isName";
obj->name = @"name";
obj->isName = @"isName";
NSLog(@"getter value:%@",[obj valueForKey:@"name"]);

逐个屏蔽有如下输出:

getter value:_name
getter value:_isName
getter value:name
getter value:isName

调用顺序优先级:_ -> _is -> -> is

5. 数据类型返回处理
如果取回的是一个对象指针,则直接返回这个结果。如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。

6. 以上都没有实现进入 valueForUndefinedKey:

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"%s --- key:%@",__func__,key);
    return nil;
}

实现valueForUndefinedKey后就不会崩溃了。可以在这里进行异常收集和处理。

总结

  • 设值:

    • 1.set相关方法调用设置,优先级set -> _set -> setIs
    • 2.先判断是否允许访问实例变量,允许的情况下给对应的实例变量赋值,优先级_ -> _is -> -> is
    • 3.在没有相关set方法以及实例变量(不允许访问也包括)会判断setValue:forUndefinedKey:是否实现进行调用,没有实现的情况下直接crash报错,实现了就交给它处理。
  • 取值:

    • 1.get相关方法调用获取值,优先级get -> -> is -> _
    • 2.普通类型方法没有找到,排查countOf是否实现,实现的情况下判断objectInAtIndex:/AtIndexes:是否有一个实现。这个时候就相当于是对数组的操作了。优先级objectInAtIndex -> AtIndexes:
    • 3.没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找countOf、enumeratorOf、memberOf命名的方法。相当于是一个NSSet
    • 4.如果允许访问实例变量,则从实例变量取值,优先级_ -> _is -> -> is
    • 5.对返回的值进行判断,是指针直接返回;负责基础类型能被NSNumber支持就存储为NSNumber返回,不能则用NSValue存储返回。
    • 6.在以上操作都失败的情况下判断 valueForUndefinedKey:有没有实现,实现的情况下交给valueForUndefinedKey:处理异常,否则报错crash

kvc取值设置流程:

kvc普通对象设值流程

kvc普通对象取值流程

三、自定义kvc

上面分析了kvc的实现原理,那么如果按照这个规则自己可以实现一套(仅针对基本类型)。
NSObject添加一个分类HP_KVC

3.1自定义kvc设值实现

按照上面的分析实现要分为4步:
1.参数容错处理

if (key == nil || key.length == 0) return;

key值不存在的时候直接返回。

2.set相关方法判断调用 set -> _set -> setIs

// set -> _set -> setIs
- (BOOL)hp_handleSetMethodsWithKey:(NSString *)key value:(id)value {
    //首字母大写
    NSString *Key = key.capitalizedString;
    //拼接方法名
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    //方法调用
    if ([self hp_performSelectorWithMethodName:setKey value:value]) {
        return YES;
    } else if ([self hp_performSelectorWithMethodName:_setKey value:value]) {
        return YES;
    } else if ([self hp_performSelectorWithMethodName:setIsKey value:value]) {
        return YES;
    }
    return NO;
}

- (BOOL)hp_performSelectorWithMethodName:(NSString *)methodName value:(id)value {
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}
  • 根据key值拼接方法进行调用,有实现返回YES,没有实现返回NO

3.实例变量调用

// 2.实例变量调用
- (BOOL)hp_handleSetIvarsWithKey:(NSString *)key value:(id)value {
    // 2.1 accessInstanceVariablesDirectly
    if (![self.class hp_accessInstanceVariablesDirectly]) {//不能调用实例变量
        //@TODO 这里需要补充最后的异常处理逻辑
        return YES;
    }
    // 2.2 _ -> _is ->  -> is
    //获取实例变量名
    NSMutableArray *ivarNamesArray = [self hp_getIvarListName];
    //首字母大写
    NSString *Key = key.capitalizedString;
    //拼接方法名
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    
    //判断ivarNamesArray中是否存在实例变量
    if ([ivarNamesArray containsObject:_key]) {
        // 获取ivar
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 实例变量设置值
        object_setIvar(self , ivar, value);
        return YES;
    } else if ([ivarNamesArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        object_setIvar(self , ivar, value);
        return YES;
    } else if ([ivarNamesArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        object_setIvar(self , ivar, value);
        return YES;
    } else if ([ivarNamesArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        object_setIvar(self , ivar, value);
        return YES;
    }
    return NO;
}


+ (BOOL)hp_accessInstanceVariablesDirectly {
    return YES;
}

- (NSMutableArray *)hp_getIvarListName {
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName: %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}
  • 先判断hp_accessInstanceVariablesDirectly是否允许访问实例变量。
  • 不允许则进行异常处理逻辑。
  • 允许则根据key拼接_ -> _is -> -> is,获取类的所有实例变量名进行比较匹配。匹配到就设值,没有匹配到就进行下一步。

4.异常处理

- (void)hp_setValue:(id)value forUndefinedKey:(NSString *)key;

//异常处理
- (void)hp_handleSetExceptionWithKey:(NSString *)key value:(id)value {
    if (![self hp_performSelectorWithMethodName:@"hp_setValue:forUndefinedKey:" value:key]) {
        @throw [NSException exceptionWithName:@"HP_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key : %@,value:%@",self,NSStringFromSelector(_cmd),key,value] userInfo:nil];
    }
}
  • 实现了hp_setValue : forUndefinedKey :就调用,否则抛出异常。

hp_setValue:forKey:实现如下:

- (void)hp_setValue:(nullable id)value forKey:(NSString *)key {
    // 0.参数容错处理
    if (key == nil || key.length == 0) return;
    // 1.set相关方法调用 set -> _set -> setIs
    if ([self hp_handleSetMethodsWithKey:key value:value]) {
        return;
    }
    // 2.实例变量调用
        // 2.1 accessInstanceVariablesDirectly
        // 2.2 _ -> _is ->  -> is
    if ([self hp_handleSetIvarsWithKey:key value:value]) {
        return;
    }
    // 3.异常处理 setValue:forUndefinedKey:
    [self hp_handleSetExceptionWithKey:key value:value];
}

3.2 自定义kvc取值实现

1.参数容错处理

if (key == nil || key.length == 0) return nil;

key值不存在的时候直接返回nil

2.get相关方法处理

    //key 首字母大写
    NSString *Key = key.capitalizedString;
    //拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    //方法调用
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(key)]) {
        return [self performSelector:NSSelectorFromString(key)];
    } else if ([self respondsToSelector:NSSelectorFromString(isKey)]) {
        return [self performSelector:NSSelectorFromString(isKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(_key)]) {
        return [self performSelector:NSSelectorFromString(_key)];
    }
#pragma clang diagnostic pop
  • 拼接方法判断调用然后返回方法的返回值。

3.实例变量取值

    //实例变量取值
    // 2.1 accessInstanceVariablesDirectly
    if (![self.class hp_accessInstanceVariablesDirectly]) {//不能调用实例变量
          //@TODO异常处理
    }
    // 2.2 实例变量进行取值
    NSMutableArray *ivarNamesArray = [self hp_getIvarListName];
    // _ -> _is ->  -> is
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    if ([ivarNamesArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    } else if ([ivarNamesArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    } else if ([ivarNamesArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    } else if ([ivarNamesArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }
  • 首先根据hp_accessInstanceVariablesDirectly判断能否从实例变量取值。
  • 拼接实例变量名称,获取所有实例变量名。
  • 获取实例变量的值。

4.异常处理

- (id)hp_handleGetExceptionWithKey:(NSString *)key {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(@"hp_valueForUndefinedKey:")]) {
        return [self performSelector:NSSelectorFromString(@"hp_valueForUndefinedKey:") withObject:key];
    }
#pragma clang diagnostic pop
    @throw [NSException exceptionWithName:@"HP_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ hp_valueForUndefinedKey]: this class is not key value coding-compliant for the key : %@",self,key] userInfo:nil];
}
  • 判断hp_valueForUndefinedKey:是否实现,实现了则调用。
  • 没有实现报错。

3.3 自定义kvc完整实现

@interface NSObject (HP_KVC)

@property (class, readonly) BOOL hp_accessInstanceVariablesDirectly;

- (void)hp_setValue:(nullable id)value forKey:(NSString *)key;

- (void)hp_setValue:(id)value forUndefinedKey:(NSString *)key;

- (id)hp_valueForKey:(NSString *)key;

- (id)hp_valueForUndefinedKey:(NSString *)key;

@end

#import "NSObject+HP_KVC.h"
#import 

@implementation NSObject (HP_KVC)

- (void)hp_setValue:(nullable id)value forKey:(NSString *)key {
    // 0.参数容错处理
    if (key == nil || key.length == 0) return;
    // 1.set相关方法调用 set -> _set -> setIs
    if ([self hp_handleSetMethodsWithKey:key value:value]) {
        return;
    }
    // 2.实例变量调用
        // 2.1 accessInstanceVariablesDirectly
        // 2.2 _ -> _is ->  -> is
    if ([self hp_handleSetIvarsWithKey:key value:value]) {
        return;
    }
    // 3.异常处理 setValue:forUndefinedKey:
    [self hp_handleSetExceptionWithKey:key value:value];
}


// set -> _set -> setIs
- (BOOL)hp_handleSetMethodsWithKey:(NSString *)key value:(id)value {
    //首字母大写
    NSString *Key = key.capitalizedString;
    //拼接方法名
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    //方法调用
    if ([self hp_performSelectorWithMethodName:setKey value:value]) {
        return YES;
    } else if ([self hp_performSelectorWithMethodName:_setKey value:value]) {
        return YES;
    } else if ([self hp_performSelectorWithMethodName:setIsKey value:value]) {
        return YES;
    }
    return NO;
}

- (BOOL)hp_performSelectorWithMethodName:(NSString *)methodName value:(id)value {
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

// 2.实例变量调用
- (BOOL)hp_handleSetIvarsWithKey:(NSString *)key value:(id)value {
    // 2.1 accessInstanceVariablesDirectly
    if (![self.class hp_accessInstanceVariablesDirectly]) {//不能调用实例变量
        //@TODO 这里需要补充最后的异常处理逻辑
        [self hp_handleSetExceptionWithKey:key value:value];
        return YES;
    }
    // 2.2 _ -> _is ->  -> is
    //获取实例变量名
    NSMutableArray *ivarNamesArray = [self hp_getIvarListName];
    //首字母大写
    NSString *Key = key.capitalizedString;
    //拼接方法名
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    
    //判断ivarNamesArray中是否存在实例变量
    if ([ivarNamesArray containsObject:_key]) {
        // 获取ivar
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 实例变量设置值
        object_setIvar(self , ivar, value);
        return YES;
    } else if ([ivarNamesArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        object_setIvar(self , ivar, value);
        return YES;
    } else if ([ivarNamesArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        object_setIvar(self , ivar, value);
        return YES;
    } else if ([ivarNamesArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        object_setIvar(self , ivar, value);
        return YES;
    }
    return NO;
}


+ (BOOL)hp_accessInstanceVariablesDirectly {
    return YES;
}

- (NSMutableArray *)hp_getIvarListName {
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName: %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

//3 异常处理
- (void)hp_handleSetExceptionWithKey:(NSString *)key value:(id)value {
    if (![self hp_performSelectorWithMethodName:@"hp_setValue:forUndefinedKey:" value:key]) {
        @throw [NSException exceptionWithName:@"HP_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key : %@,value:%@",self,NSStringFromSelector(_cmd),key,value] userInfo:nil];
    }
}

- (id)hp_valueForKey:(NSString *)key {
    //0 key容错处理
    if (key == nil || key.length == 0) return nil;
    //1 get相关方法处理
    //key 首字母大写
    NSString *Key = key.capitalizedString;
    //拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    //方法调用
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(key)]) {
        return [self performSelector:NSSelectorFromString(key)];
    } else if ([self respondsToSelector:NSSelectorFromString(isKey)]) {
        return [self performSelector:NSSelectorFromString(isKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(_key)]) {
        return [self performSelector:NSSelectorFromString(_key)];
    }
#pragma clang diagnostic pop
    //实例变量取值
    // 2.1 accessInstanceVariablesDirectly
    if (![self.class hp_accessInstanceVariablesDirectly]) {//不能调用实例变量
        return [self hp_handleGetExceptionWithKey:key];
    }
    // 2.2 实例变量进行取值
    NSMutableArray *ivarNamesArray = [self hp_getIvarListName];
    // _ -> _is ->  -> is
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    if ([ivarNamesArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    } else if ([ivarNamesArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    } else if ([ivarNamesArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    } else if ([ivarNamesArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }
    // 3 异常处理
    return [self hp_handleGetExceptionWithKey:key];
}

- (id)hp_handleGetExceptionWithKey:(NSString *)key {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(@"hp_valueForUndefinedKey:")]) {
        return [self performSelector:NSSelectorFromString(@"hp_valueForUndefinedKey:") withObject:key];
    }
#pragma clang diagnostic pop
    @throw [NSException exceptionWithName:@"HP_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ hp_valueForUndefinedKey]: this class is not key value coding-compliant for the key : %@",self,key] userInfo:nil];
}

@end

当然这里只是实现了基本类型以及单层path的逻辑。github上有很多自定的实现,原理都差不多。具体可以参考别人比较优秀的实现:DIS_KVC_KVO

四、kvc小技巧

typedef struct {
    float x, y, z;
} HPPoint;

@interface HotpotCat : NSObject

@property (nonatomic, copy) NSString *name;

@end

@interface HPObject : NSObject {
    @public
    NSString *nickName;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) double height;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, assign) HPPoint point;
@property (nonatomic, strong) HotpotCat *hotpotCat;
@property (nonatomic, assign) bool married;

@end

4.1 kvc 自动转换类型

HPObject *obj = [HPObject alloc];
//age是int类型,需要转成NSNumber
[obj setValue:@18 forKey:@"age"];
NSLog(@"int -> number value:%@ type:%@",[obj valueForKey:@"age"],[[obj valueForKey:@"age"] class]);
//由于age是int类型,存的是字符串@"20",kvc自动转换成NSCFNumber
[obj setValue:@"20" forKey:@"age"];
NSLog(@"string -> number value:%@ type:%@",[obj valueForKey:@"age"],[[obj valueForKey:@"age"] class]);

//由于sex是Bool类型,存的是字符串@“20”,KVC自动转换成NSCFNumber
[obj setValue:@"20" forKey:@"married"];
NSLog(@"bool -> number value:%@ type:%@",[obj valueForKey:@"sex"],[[obj valueForKey:@"sex"] class]);

输出:

int -> number value:18 type:__NSCFNumber
string -> number value:20 type:__NSCFNumber
BOOL -> number value:1 type:__NSCFBoolean

由于ageint类型需要转换为NSNumber可以理解。在用NSStringage赋值时,自动转换为了NSCFNumber。在用NSStringbool类型赋值时自动转换为了NSCFBoolean类型。

4.2 kvc 设置空值

[obj setValue:nil forKey:@"age"];
[obj setValue:nil forKey:@"name"];

当给age设置nil时直接报错:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[ setNilValueForKey]: could not set nil as the value for the key age.'

name设置成功,没有任何输出。在HPObject中实现setNilValueForKey

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"%s key:%@",__func__,key);
}

这个时候age设置空值会进入到setNilValueForKeyname不会。
根据官方的注释setNilValueForKey只能处理NSNumberNSValue结构体。

4.3 找不到key

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"%s key:%@, value:%@",__func__,key,value);
}
 
- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"%s key:%@",__func__,key);
    return nil;
}

取值和设置找不到key的时候分别对应valueForUndefinedKey:setValue:forUndefinedKey:。可以在这里进行容错处理。

4.4 键值验证

验证能不能设置某个值:

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

先找一下类中是否实现了方法-(BOOL)validate:error:,实现了就会根据实现方法里面的自定义逻辑返回NO/YES,没有实现这个方法,系统默认返回YES
比如对于name而言:

- (BOOL)validateName:(id *)value error:(out NSError *__autoreleasing  _Nullable *)outError {
    NSLog(@"%s",__func__);
    NSString *name = *value;
    if ([name isEqualToString:@"HP"]) {
        return YES;
    }
    return NO;
}

//调用
NSError *error;
NSString *name = @"HP1";
if (![obj validateValue:&name forKey:@"name" error:&error]) {
   NSLog(@"%@",error);
} else {
   NSLog(@"%@",[obj valueForKey:@"name"]);
}

只有nameHP的时候才验证通过。

重写同时验证key和value:
当然也可以重写validateValue: validateValue : error:,重写后就不走-(BOOL)validate:error:的逻辑了:

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    if([inKey isEqualToString:@"name"]){
        [self setValue:[NSString stringWithFormat:@"HP_%@",*ioValue] forKey:inKey];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"key not allowed set:%@ - %@ ",inKey,self] code:10086 userInfo:nil];
    return NO;
}

可以在这里进行键值验证、容错 、 派发 、 消息转发处理。

你可能感兴趣的:(Key-Value Coding (KVC)原理)