KVC底层原理

前言

KVC(Key-Value-Coding)键值编码,在iOS中应用及其广泛,我们通常使用KVC设置值获取值,知道KVC的基本运作原理对我们在日常开发中修复bug起到很好的作用。

KVC之取值

valueForKey

首先,来看一个简单问题,创建一个Person类。

Person.h:

1
2
3
4
5
#import <Foundation/Foundation.h>

@interface Person : NSObject

@end

Person.m:

1
2
3
4
5
#import "Person.h"

@implementation Person

@end

ViewController.m中,导入Person.hViewController.m内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}

- (void)test
{
Person *p = [[Person alloc] init];
// 注意,获取的时候这个key必须是成员变量的原形!!!
// 比如,如果这里不是name, 而是下划线 _name 那么就要就Person类
// 里面的成员变量一定要有_name,否则就会奔溃!
NSString *name = [p valueForKey:@"name"];
NSLog(@"name is %@", name);
}

@end

在运行之前,先思考一下结果是什么?

答案是:程序崩溃,抛出异常!

1
2
3
*** Terminating app due to uncaught exception 'NSUnknownKeyException', 
reason: '[<Person 0x600002c35340> valueForUndefinedKey:]: this class
is not key value coding-compliant for the key name.'

为什么会这样呢?我们来分析一下:实例对象p并没有这个name成员变量或者说没有这个属性,在调用KVCvalueForKey方法时,由于找不到name从而执行了- (id)valueForUndefinedKey:(NSString *)key这个方法,接着就抛出了异常,因此程序崩溃。

不信,我们来验证一下,在Person.h中,复写- (id)valueForUndefinedKey:(NSString *)key方法,于是Person.m的内容就变成:

1
2
3
4
5
6
7
8
9
#import "Person.h"

@implementation Person

- (id)valueForUndefinedKey:(NSString *)key {
return @"hello, world!";
}

@end

再次运行,输出结果:

1
name is hello, world!

哇,果然如此!当使用valueForKey方法时,如果查找不到相应的属性或成员变量,就直接走valueForUndefinedKey抛出异常。

问题是,KVC是如何查找属性或成员变量的呢?我们来继续验证:

Person.h中,定义name成员变量,并在Person.m的初始化方法中给name赋值。

于是,Person.h

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface Person : NSObject
{
NSString *name;
}
@end

Person.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "Person.h"

@implementation Person

- (instancetype)init
{
self = [super init];
if (self) {
name = @"name";
}
return self;
}

@end

运行程序,结果是:

1
name is name

一点也不意外,按正常的逻辑执行,我们再添加一个_name成员变量并赋值

Person.h

1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>

@interface Person : NSObject
{
NSString *name;
NSString *_name;
}
@end

Person.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import "Person.h"

@implementation Person

- (instancetype)init
{
self = [super init];
if (self) {
_name = @"_name";
name = @"name";
}
return self;
}

@end

运行程序,结果是什么呢?

结果是:

1
name is _name

到这里,我们可以确定_name的优先级比name的优先级要高。

同理,我们可以添加成员变量_isNameisName,同上面比较的方法一样比较(这里就不继续演示了),可以发现成员变量的查找优先级如下:

1
_name > _isName > name > isName

也就是说,KVC中,查找成员变量或属性的方式是,首先查找_key,如果找不到,继续查找_isKey,再找不到,继续查找key,还是查找不到,才查找isKey,如果isKey都找不到的话,程序将抛出异常(除非你重写了- (id)valueForUndefinedKey:(NSString *)key方法)。


我们知道,在Objective-C中,有一个东西叫属性,而属性的本质就是对成员变量settergetter方法的封装,那么当我们通过KVC调用valueForKey方法获取实例变量的时候,而获取的方式很类似getter,因为getter就表示获取的意思,那么,它与getter方法有什么关系呢?

Person.h

1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>

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

Person.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#import "Person.h"

@implementation Person

- (instancetype)init
{
self = [super init];
if (self) {
_isName = @"init~_isName";
_name = @"init~_name";
name = @"init~name";
isName = @"init~isName";
}
return self;
}

- (NSString *)getName
{
return @"getName";
}

- (NSString *)name
{
return @"name";
}

- (NSString *)isName
{
return @"isName";
}

- (NSString *)_getName
{
return @"_getName";
}

- (NSString *)_name
{
return @"_name";
}

@end

运行程序,输出结果如下:

1
name is getName

很明显,当通过valueForKey获取name成员变量,若有- (NSString *)getName方法存在,则优先执行这个方法,并且不再去获取实例变量在- (instancetype)init的初始化值。另外值得注意的是,执行了- (NSString *)getName方法,说明其优先级大于- (NSString *)name- (NSString *)isName- (NSString *)_getName- (NSString *)_name方法。

我们去掉- (NSString *)getName方法,再次运行,输出结果如下:

1
name is name

接着去掉- (NSString *)name方法,输出结果如下:

1
name is isName

接着去掉- (NSString *)isName方法,输出结果如下:

1
name is _getName

接着去掉- (NSString *)_getName方法,输出结果如下:

1
name is _name

结果表明,只要- (NSString *)getName- (NSString *)name- (NSString *)isName- (NSString *)_getName- (NSString *)_name中的任何一个方法存在,就不再去获取初始化中的name成员变量的值。

也就是说,在KVC中,只要存在- (NSString *)getKey- (NSString *)key- (NSString *)isKey- (NSString *)_getKey- (NSString *)_key方法,其优先级都比成员变量高,其中优先级顺序是:- (NSString *)getKey > - (NSString *)key > - (NSString *)isKey > - (NSString *)_getKey > - (NSString *)_key

事实上,- (NSString *)getKey > - (NSString *)key- (NSString *)isKey- (NSString *)_getKey- (NSString *)_key在底层本质上都是getter方法,就如同查找成员变量一样,Apple提供了多种获取方式。

还有一点要特别注意,当没有以上的相关方法时,我们知道valueForKey会去查找实例变量的值,但是,在查找实例变量之前,其实还执行了一个方法,它是+ (BOOL)accessInstanceVariablesDirectly并且其默认返回值为YES,如果既没有上面的gettter方法,并且+ (BOOL)accessInstanceVariablesDirectly的返回值为NO,那么将会直接走- (id)valueForUndefinedKey:(NSString *)key抛出异常,程序崩溃。当然,一般情况下,这个方法我们不会使用到。

流程图如下:

KVC .png

valueForKeyPath

事实上,KVC除了提供valueForKey方法来获取值之外,还提供了valueForKeyPath的取值方法,区别在于valueForKeyPath可以按路径取值,比如:假设person实例对象有一个dog属性,dog自身又存在一个name属性,那么我们可以使用valueForKeyPath的取值方法来获取dogname属性

1
id dogName = [person valueForKeyPath: @"dog.name"]

这个原理差不多,就不赘述了。

KVC之设值

既然KVC可以获取值,反之,也一定可以设置值,从上面我们可以知道,KVC的取值的成员变量有4个,并且有着优先级,即:_key > _isKey > key > isKey。那么,同样的,当我们使用KVCsetValue:value forKey:key方法时,KVC也会去查找相应的4个成员变量,并且设置值的优选级完全和KVC取值一样!(前提条件是没有相应的setter方法,这个稍后会讲)

同样地,我们来测试一下:

Person.h中:

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>

@interface Person : NSObject
{
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
- (void)printName;
@end

Person.m中:

1
2
3
4
5
6
7
8
9
10
11
12
#import "Person.h"

@implementation Person

- (void)printName
{
NSLog(@"_name: %@", _name);
NSLog(@"_isName: %@", _isName);
NSLog(@"name: %@", name);
NSLog(@"isName: %@", isName);
}
@end

ViewController.m中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}

- (void)test
{
Person *p = [[Person alloc] init];
[p setValue:@"test name" forKey:@"name"];
[p printName];
}

@end

执行程序,结果如下:

1
2
3
4
_name: test name
_isName: (null)
name: (null)
isName: (null)

可以看出,先设置的是_name的值,去掉_name,就会设置_isName的值,其它同理。

需要注意的是,如果设置的成员变量不存在,那么程序将调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key抛出异常,直接奔溃,这个和KVC取值时找不到getter方法、成员变量时调用- (id)valueForUndefinedKey:(NSString *)key是一样的道理。

同理,KVC取值有+ (BOOL)accessInstanceVariablesDirectly方法,KVC设值也是同一个方法,若没有相应的setter方法,并且返回NO时,程序将调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key抛出异常,因此,为了防止异常,我们有时候可以重写- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法以防止奔溃。另外,值得一提的是,KVC在给数值型数据类型如NSInteger设值nil值的话,会调用- (void)setNilValueForKey:(NSString *)key抛出异常。比如:

Person.h中:

1
2
3
{
NSInteger age;
}

ViewController.m中:

1
2
Person *p = [[Person alloc] init];
[p setValue:nil forKey:@"age"];

毫无疑问,程序将奔溃!因此,仅仅重写- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法是不够的的,要同时重写- (void)setNilValueForKey:(NSString *)key,防止程序奔溃!

KVC取值中,有5getter方法,但是在KVC设值中,只有2setter方法,分别是:setKeysetIsKey,例如

1
2
3
4
5
6
7
8
9
- (void)setName:(NSString *)name
{
NSLog(@"setter name: %@", name);
}

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

优先级:setName > setIsName,也即是 setKey > setIsKey

KVC取值是一个道理,若有以上所说的2setter方法,则优先执行setter方法,忽略成员变量在初始化时的值。没有setter方法,就判断+ (BOOL)accessInstanceVariablesDirectly的返回值是否为NO,若为NO,调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key抛出异常(没有重写该方法则崩溃),若为YES,就去查找相应的成员变量并赋值。

手动写一个KVC

参照KVC的设值和取值的基本流程,我们也可以自己手动写一个属于自己的KVC。具体说明这里就不介绍了,有兴趣可直接查看源码,都是一些简单的业务逻辑判断,利用OCruntime机制,实现出相应的KVC效果。

查看KVC源码