本文为L_Ares个人写作,以任何形式转载请表明原文出处。
资料准备 : AppleDevelopment - KVC文档
代码准备 : 创建一个
Project--->App。创建JDPerson类。添加NSString类型的成员变量,_name,_isName,name,isName。
上一节主要介绍了一些KVC的基本信息,包括知道了KVC本身是一种间接访问机制,提供的是直接访问实例变量的setter和getter,还有一些常见或者常用的API。
本节依然从文档入手,探索KVC的访问模式是怎样的。
首先从最最常见的setValueForKey来看,在我们对一个类的某个属性利用KVC进行赋值的时候,比如说JDPerson有个name属性,访问的一般是name的setter或者getter,那么这就会影响对KVC本身的探索,所以为了纯净环境下探索KVC的访问模式,我就选择相对更纯净一点的成员变量方式,而不是通过属性的方式。
一、KVC的访问模式——设值过程
先看一下官方文档里面对于KVC访问模式中的Setter的一些知识。

根据自己的理解,加上翻译,可以将官方文档给出的三个知识点总结 :
【前言】 : setValue:forKey:这个方法的默认实现,给定key和value作为输入参数,尝试将名为key的变量的值设置为value(对于非对象属性,也就是上一节中见到过的结构体,是需要进行一步包装的),设置的流程如下所述 :
- 第一,先寻找
set或_set方法,如果找到了就调用set或者_set方法进行赋值。
- 第二,如果没有找到
set或者_set方法,并且调用KVC的对象的类的accessInstanceVariablesDirectly方法返回值是YES,那么就按照顺序查找具有如下名称的实例变量(拿name举例),_name、_isName、name、isName。如果找到这四个之一,直接对其进行赋值为value,并且完成赋值。
- 第三,如果第一,第二条全都不满足,那就调用
setValue:forUndefinedKey:。本来默认是会抛出异常的,但是NSObject的一个子类提供了这个方法来处理异常的key。
按照步骤,举个例子 :
(1). 验证上面的第1点 :
JDPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface JDPerosn : NSObject
{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
NS_ASSUME_NONNULL_END
JDPerson.m
#import "JDPerosn.h"
@implementation JDPerosn
#pragma mark - 开启或关闭实例变量的赋值
//默认就是YES,设置为NO的话,如果没有set或_set的方法,那么就无法对实例变量进行赋值
+(BOOL)accessInstanceVariablesDirectly
{
return YES;
}
#pragma mark - KVC - setKey流程
//这就是文档中说的set<key>
- (void)setName:(NSString *)name
{
NSLog(@"%s---%@",__func__,name);
}
//_set<key>
- (void)_setName:(NSString *)name
{
NSLog(@"%s---%@",__func__,name);
}
- (void)setIsName:(NSString *)name
{
NSLog(@"%s---%@",__func__,name);
}
@end
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
JDPerosn *person = [[JDPerosn alloc] init];
//1. KVC - 设置值的过程
[person setValue:@"LJD" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,
person->name,person->isName);
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
NSLog(@"%@-%@",person->name,person->isName);
NSLog(@"%@",person->isName);
}
执行结果 :

可以再把- (void)setName:(NSString *)name方法注释掉,会发现调用- (void)_setName:(NSString *)name,如果把它也注释掉,则会调用- (void)setIsName:(NSString *)name。4个实例变量的值依然都是null。
这就验证了上面说的第一点,在利用KVC设置值的时候,会先找到set或者_set方法对其进行赋值。
(2). 验证上面的第二点 :
把JDPerson.m中的有关set的3个方法全部注释掉,然后让accessInstanceVariablesDirectly变成return NO。其余代码不变,再执行。结果如下图 :

所以验证了第二点中,没有set或_set方法的时候,accessInstanceVariablesDirectly要设置成YES才可以。
然后将accessInstanceVariablesDirectly重新设置成YES。再执行。结果如下图 :

验证了第二点中会先给_name赋值。
然后依次的注释掉JDPerson.h中的成员变量_name、_isName、name。
就会验证第二点中,KVC赋值同名变量的顺序是_name、_isName、name、isName。
第三点我就不验证了,因为经常会用到,比如字典转模型的时候,大家应该常用。
另外,KVC为什么有第二点这种设计呢?因为在编译期的时候,底层也会生成一些这样的变量,比如之前我们在前面的章节进行clang将.m文件编译成.cpp文件的时候,有一些属性就会变成了_xxx的形式的成员变量,或者BOOL类型的值会变成isXXX的形式,这也是因为运行时的特性造成的。
总结 :

二、KVC的访问模式——取值过程
一样先来查阅官方文档,然后根据自己的理解加上翻译,再做总结。

明显getter的过程比较多。还是按照官方的分成六点。
【前言】 : valueForKey:的默认实现,给定key作为输入参数,执行下列程序,从接收valueForKey:调用的类实例的内部操作。
- 第一,按照顺序查找,是否有
get<Key>, <key>, is<Key>,_<key>这些方法。如果找到了就调用它,并且在下面的第五点中处理结果。如果找不到就进入第二点。
- 第二,如果
第一点中的4个get方法都没实现,在实例方法中查找countOf<Key>、objectIn<Key>AtIndex:还有<key>AtIndexes:方法。
- 如果
countOf<Key>存在,并且objectIn<Key>AtIndex:和<key>AtIndexes:中至少一个方法存在,那么创建一个响应所有的NSArray方法的集合代理对象,并且返回这个集合代理对象。- 否则进入
第三点。集合代理对象随后会将所有接收到的NSArray的消息都转换为countOf、objectInAtIndex:和AtIndexes:的一些组合,这些组合将消息发送给创建它的符合KVC机制的对象。- 如果原始对象还实现了一个名为
get:range:的可选方法,代理对象也会在适当的时候使用它。- 实际上,代理对象与和
KVC兼容的对象一起工作,允许底层属性像NSArray一样工作,即使它不是NSArray。
- 第三,如果没有找到
第二点中的3个方法,则同时查找countOf <Key>,enumeratorOf<Key>和memberOf<Key>这三个方法,
- 如果这三个方法都找到了,创建一个响应所有
NSSet方法的集合代理对象,并将集合代理对象返还。- 如果这三个方法也没找到,那么就直接进入
第四点。集合代理对象随后将它接收到的任何NSSet消息转换为countOf、enumeratorOf和memberOf:消息的组合,并发送给创建它的对象。- 实际上,代理对象与遵循
KVC的对象一起工作,允许底层属性像NSSet一样运行,即使它不是NSSet。
- 第四,如果
get方法和集合方法都没找到,并且接收者的类方法accessinstancevariables返回YES,则按照顺序查找_<key>, _is<Key>, <key>, is<Key>变量。如果找到,直接获取实例变量的值并进入第五点。否则,进入第六点。
- 第五,根据搜索到的属性值的类型,返回不同的结果
- 如果是
对象指针,则直接返回结果。- 如果值是
NSNumber支持的标量类型,将其存储在NSNumber实例中并返回。- 如果结果是
NSNumber不支持的标量类型,转换为NSValue对象并返回它。
- 第六,如果上面的所有方法都没有找到,调用
setValue:forUndefinedKey:。默认情况下会抛出异常,可以执行这个方法来处理。
因为这里明显的能看出来,简单类型取值和集合类型是不一样的,先看简单类型,按照步骤,举个例子 :
(1). 验证上面的第1点和第4点 :
JDPerson.h不发生改变。
JDPerson.m
#pragma mark - 开启或关闭实例变量的赋值
//默认就是YES,设置为NO的话,如果没有set或_set的方法,那么就无法对实例变量进行赋值
+(BOOL)accessInstanceVariablesDirectly
{
return YES;
}
#pragma mark - KVC - getKey流程
//文档中说的get<key>
- (NSString *)getName
{
return NSStringFromSelector(_cmd);
}
//\<key>
- (NSString *)name
{
return NSStringFromSelector(_cmd);
}
//is<key>
- (NSString *)isName
{
return NSStringFromSelector(_cmd);
}
//_<key>
- (NSString *)_name
{
return NSStringFromSelector(_cmd);
}
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self jd_kvc_getter];
}
- (void)jd_kvc_getter
{
JDPerosn *person = [[JDPerosn alloc] init];
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"KVC取值---%@",[person valueForKey:@"name"]);
}
- (void)jd_kvc_setter
{
JDPerosn *person = [[JDPerosn alloc] init];
//1. KVC - 设置值的过程
[person setValue:@"LJD" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,
person->name,person->isName);
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
NSLog(@"%@-%@",person->name,person->isName);
NSLog(@"%@",person->isName);
}
执行结果 :

然后依次注释掉4个get方法,就可以验证第一点,并且全部注释掉以后会走到第四点,顺便把第四点也验证了。
从验证可以看出,
valueForKey先找的不是我们对变量的赋值,而是先找的getter方法,只有getter方法没有实现的情况下,才会找到变量直接拿值。简单类型也就是非集合类型的只会找到第一点和第四点如果有错误会直接到第六点。
(1). 验证上面的第2点和第3点 :
因为第二,第三点的原理是一样的,这里就拿更常见的数组来说明,NSSet的例子会放出来,但是就不说明了。
JDPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface JDPerosn : NSObject
{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
NSArray *arr;
NSSet *set;
}
@end
NS_ASSUME_NONNULL_END
JDPerson.m
#pragma mark - KVC - 正常的数组取值
- (NSUInteger)countOfArr
{
NSLog(@"%s",__func__);
return [arr count];
}
- (id)objectInArrAtIndex:(NSUInteger)index
{
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"objectInArrAtIndex : %lu",index];
}
#pragma mark - KVC - 正常的集合取值
- (NSUInteger)countOfSet
{
NSLog(@"%s",__func__);
return [set count];
}
- (id)memberOfSet:(id)object
{
NSLog(@"%s",__func__);
return [set containsObject:object] ? object : nil;
}
- (id)enumeratorOfSet
{
NSLog(@"%s",__func__);
return [set objectEnumerator];
}
#pragma mark - KVC - 没有Pens,但是我有方法,一样可以取值数组
- (NSUInteger)countOfPens
{
NSLog(@"%s",__func__);
return [arr count];
}
- (id)objectInPensAtIndex:(NSUInteger)index
{
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"objectInPensAtIndex %lu", index];
}
- (id)pensAtIndexes:(NSUInteger)index
{
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"pensAtIndexes %lu", index];
}
#pragma mark - KVC - 没有books,但是我有方法,一样可以取值集合
// 个数
- (NSUInteger)countOfBooks{
NSLog(@"%s",__func__);
return [arr count];
}
// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
NSLog(@"%s",__func__);
return [set containsObject:object] ? object : nil;
}
// 迭代器
- (id)enumeratorOfBooks {
// objectEnumerator
NSLog(@"来了 迭代编译");
return [arr reverseObjectEnumerator];
}
ViewController.m
- (void)jd_kvc_array_and_set
{
JDPerosn *person = [[JDPerosn alloc] init];
person->arr = @[@"pen0", @"pen1", @"pen2", @"pen3"];
NSLog(@"正常的数组取值 : %@",[person valueForKey:@"arr"]);
NSArray *array = [person valueForKey:@"pens"];
NSLog(@"就算没有pens,只要在类中实现了方法也可以objectAtIndex : %@",[array objectAtIndex:1]);
NSLog(@"就算没有pens,只要在类中实现了方法也可以containsObject : %d",[array containsObject:@"pen1"]);
person->set = [NSSet setWithArray:person->arr];
NSLog(@"正常的集合取值 : %@",[person valueForKey:@"set"]);
NSSet *set = [person valueForKey:@"books"];
[set enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"set遍历 %@",obj);
}];
}
执行结果就不贴图了,太长了,可以自己运行一下,会发现count的方法都会打印两次,也证明了的确是存在着代理集合的。所以验证也是符合的。
总结 :
KVC的valueForKey的流程主要还是分了简单对象和集合对象,简单对象的取值和其setValueForKey是一样的。而集合对象则多需要几步骤,但是可以防止取到不认识的key,也可以修改取值的结果。

三、KVC的一些特殊功能
1. KVC有自动转换类型的功能。
JDPerson.h :
typedef struct {
float x, y, z;
} ThreeFloats;
@interface JDPerosn : NSObject
{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
NSArray *arr;
NSSet *set;
int age;
ThreeFloats threeFloats;
}
@end
1.1 NSNumber支持的标量类型
//1. NSNumber支持的标量类型
JDPerosn *person = [[JDPerosn alloc] init];
[person setValue:@18 forKey:@"age"];
NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
[person setValue:@"20" forKey:@"age"];
NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber

证明了 :
KVC在对已知类型的value设置的时候,如果类型是NSNumber支持的标量类型,则会将value存储到NSNumber的实例中,在取值的时候返回NSNumber实例。
1.2 NSNumber不支持的标量类型
//2. NSNumber不支持的标量类型
ThreeFloats floats = {1.f, 2.f, 3.f};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue

证明了 :
KVC存储的类型如果是NSNumber不支持的标量类型,那么就要转换为NSValue存储并且返回。
2. 空值
在JDPerson.m中添加KVC监控 :
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"设置 %@ 是空值",key);
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"没有这个key : %@",key);
}
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"没有这个key : %@ - 给你一个其他的吧,别奔溃了!",key);
return @"LJD";
}
2.1 key存在,value设置为空
在-(void)viewDidLoad中 :
JDPerosn *person = [[JDPerosn alloc] init];
[person setValue:nil forKey:@"age"]; // subject不会走 - 官方注释里面说只对 NSNumber - NSValue
[person setValue:nil forKey:@"name"];


证明了 :
KVC中的setNilValueForKey只监控NSNumber和NSValue的结构体,不会监控到其他类型。
2.2 key不存在,value设置为空
[person setValue:nil forKey:@"LJD"]; //LJD不是已知的key
可以实现这个 :
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
进行监控。

2.3 key为空
NSLog(@"%@",[person valueForKey:@"KC"]); //KC是不存在的key
这个可以使用 :
- (id)valueForUndefinedKey:(NSString *)key
进行监控。

3. 键值验证
JDPerson.m中添加 :
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if([inKey isEqualToString:@"name"]){
[self setValue:[NSString stringWithFormat:@"可以修改一下: %@",*ioValue] forKey:inKey];
return YES;
}
*outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:16688 userInfo:nil];
return NO;
}
viewDidLoad中 :
NSError *error;
NSString *name = @"LJD";
JDPerosn *person = [[JDPerosn alloc] init];
if (![person validateValue:&name forKey:@"names" error:&error]) {
NSLog(@"%@",error);
}else{
NSLog(@"%@",[person valueForKey:@"name"]);
}
结果 :
