【iOS】KVC和KVO的应用及本质

对于私有属性或成员变量如何进行读写?

KVC

KVC(Key Value Coding)键值编码,可以通过Key名直接读写对象属性或成员变量,即使有些成员变量是私有的也可以。KVC是一个基于NSKeyValueCoding非正式协议实现的机制,KVC的定义是通过NSObject的分类NSKeyValueCoding实现的,所以一切继承自NSObject的对象都可以实现KVC。

案例一(属性)

案例一声明:

1
2
3
4
5
6
7
8
9
10
@interface YBPerson : NSObject

@property (nonatomic, copy) NSString *name; // 姓名
@property (nonatomic, assign) NSInteger age; // 年龄

@end

@implementation YBPerson

@end

案例一使用:

1
2
3
4
5
YBPerson *person = [[YBPerson alloc] init];
person.name = @"idbeny";
person.age = 10;
NSLog(@"%@-%ld", person.name, person.age);
// 输出:idbeny-10

我们正常访问一个属性是通过上述点语法的方式,如果是成员变量如何访问呢?

案例二(成员变量-公共)

案例二声明:

1
2
3
4
5
6
7
8
9
10
11
12
@interface YBPerson : NSObject
{
@public
NSString *_name;
}
@property (nonatomic) NSInteger age;

@end

@implementation YBPerson

@end

案例二使用:

1
2
3
4
5
YBPerson *person = [[YBPerson alloc] init];
person -> _name = @"idbeny";
person.age = 10;
NSLog(@"%@-%ld", person -> _name, person.age);
// 输出:idbeny-10

上面案例二可以看到成员变量使用了@public关键词修饰,如果不修饰或使用@private还能正常读写么?

案例三(成员变量-私有)

案例三声明:

1
2
3
4
5
6
7
8
9
10
11
12
@interface YBPerson : NSObject
{
@private // 可以不写,默认就是私有的
NSString *_name;
}
@property (nonatomic) NSInteger age;

@end

@implementation YBPerson

@end

案例三使用:

1
2
3
4
YBPerson *person = [[YBPerson alloc] init];
person -> _name = @"idbeny";
person.age = 10;
NSLog(@"%@-%ld", person -> _name, person.age);

案例三代码编译报错
访问私有变量

案例四(KVC)

案例四声明:

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
// 人
@interface YBPerson : NSObject
{
@private
NSString *_name; // 姓名
YBPet *_pet; // 宠物
}
@property (nonatomic) NSInteger age; // 年龄

@end

@implementation YBPerson

@end

// 宠物
@interface YBPet : NSObject

@property (nonatomic, copy) NSString *type; // 宠物类型

@end

@implementation YBPet

@end

案例四使用:

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
YBPerson *person = [[YBPerson alloc] init];
YBPet *pet = [[YBPet alloc] init];

// setValue:forKey:、valueForKey:
[person setValue:@"idbeny-key" forKey:@"name"];
[person setValue:pet forKey:@"pet"];
NSLog(@"%@-%@", [person valueForKey:@"name"], [person valueForKey:@"pet"]);
// 输出:idbeny-key-<YBPet: 0x6000017658a0>

// setValue:forKeyPath:、valueForKey:、valueForKeyPath:
[person setValue:@"idbeny-keyPath" forKeyPath:@"name"];
[person setValue:@"dog" forKeyPath:@"pet.type"];
NSLog(@"%@-%@", [person valueForKey:@"name"], [person valueForKeyPath:@"pet.type"]);
// 输出:idbeny-keyPath-dog

// setValuesForKeysWithDictionary:、dictionaryWithValuesForKeys
NSDictionary *dict = @{
@"name" : @"idbeny-dict",
@"age" : @"10"
};
[person setValuesForKeysWithDictionary:dict];
NSLog(@"%@", [person dictionaryWithValuesForKeys:@[@"name", @"age"]]);
// 输出:{age = 10; name = "idbeny-dict";}

// 编译正常、运行报错 *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<YBPerson 0x6000032cfde0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key sex.'
[person setValue:@"1" forKey:@"sex"];

// 结果同上
[person setValue:@"1" forKeyPath:@"sex"];

// 编译正常、运行报错 *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<YBPerson 0x600000a9b600> valueForUndefinedKey:]: this class is not key value coding-compliant for the key sex.'
[person valueForKey:@"sex"];

// 结果同上
[person valueForKeyPath:@"sex"];

我们可以看下相关方法的注释说明,官方文档写的非常非常详细,具体就不在此翻译了,接下来只说重点。

写操作:

1
2
3
4
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
  • setValue:forKey: 该方法的key值只能是一个属性,不能多级使用(不能使用点语法);
  • setValue:forKeyPath: key值可以多级属性(使用点语法),将来系统会自动解析,该方法可以直接给单个属性赋值(即上面的方法),所以一般使用该方法;
  • setValuesForKeysWithDictionary: 可以接收一个字典,字典的key和value对应属性名和属性值;
  • setValue:forUndefinedKey: 系统自动调用,当key或keyPath找不到时,系统会触发该方法,无须我们手动调用,但我们可以利用(重写)该方法做一些拦截操作;
  • setNilValueForKey: 如果设置的value是空的,会调用该方法,同setValue:forUndefinedKey:一样,都是系统自动调用,我们可以重写进行拦截。

读操作:

1
2
3
- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForUndefinedKey:(NSString *)key;
  • valueForKey: 对应的是setValue:forKey:
  • valueForKeyPath: 对应的是setValue:forKeyPath:
  • valueForUndefinedKey: 系统自动调用,当key不存在时会触发该方法,无须我们手动调用,但我们可以利用(重写)该方法做一些拦截操作。

扩展

使用KVC还可以进行一些计算操作。虽然并不常用,但可以作为一个了解,正常开发中我们会使用C语言库中的math函数(OC已将这些标准库集成到开发环境中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
YBPerson *person1 = [[YBPerson alloc] init];
person1.age = 1;

YBPerson *person2 = [[YBPerson alloc] init];
person2.age = 3;

YBPerson *person3 = [[YBPerson alloc] init];
person3.age = 6;
NSArray *personList = @[person1, person2, person3];
id count = [personList valueForKeyPath:@"@count"];
NSLog(@"数组长度:%@", count);
// 输出:数组长度:3

id max = [personList valueForKeyPath:@"@max.age"];
NSLog(@"最大值:%@", max);
// 输出:最大值:6

id min = [personList valueForKeyPath:@"@min.age"];
NSLog(@"最小值:%@", min);
// 输出:最小值:1

id avg = [personList valueForKeyPath:@"@avg.age"];
NSLog(@"平均值:%@", avg);
// 输出:平均值:3.3333333333333333333333333333333333333

KVO

KVO(Key Value Observing)键值监听,其实是一种观察者模式,为对象的某一个属性值变化进行监听。KVO也是通过NSObject的分类NSKeyValueObserverRegistration实现的,所以一切继承自NSObject的对象都可以实现KVO。

NSKeyValueObserverRegistration一共提供了三个方法:

添加观察者

1
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
  • observer:观察者
  • keyPath:需要观察的属性
  • options:观察配置选项(观察值变化,并把需要观察的属性值传递给观察方法)
    • NSKeyValueObservingOptionNew 修改之前的值
    • NSKeyValueObservingOptionOld 修改之后的值
    • NSKeyValueObservingOptionInitial 属性被重新赋值,注册后会立马调用一次(之后只要属性被重新赋值一次就会调用一次)
    • NSKeyValueObservingOptionPrior 值修改前后各调用一次
  • context:上下文,可以携带参数(移除观察者时必须匹配)

移除观察者

1
2
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

上面的两个移除方法,一个有context,一个没有。官方建议使用removeObserver:forKeyPath:context:,因为该方法更加精确知道要移除哪个观察者。

如果观察同一个对象同一个属性不同的context,观察结束后使用removeObserver:forKeyPath:进行移除则会发生未知错误,因为系统不知道你要移除哪个观察者(毕竟上下文不一样)。如果使用context进行校验,则很清楚知道移除哪个观察者。

案例

案例声明

1
2
3
4
5
6
7
8
9
@interface YBPerson : NSObject

@property (nonatomic) NSInteger age;

@end

@implementation YBPerson

@end

案例使用

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
YBPerson *person = [[YBPerson alloc] init];
person.age = 10;
// 添加观察者(为person的age属性添加观察者self)
// kNilOptions
// [person addObserver:self forKeyPath:@"age" options:kNilOptions context:nil];

// NSKeyValueObservingOptionNew
// [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];

// NSKeyValueObservingOptionOld
// [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld context:nil];

// NSKeyValueObservingOptionInitial
// [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionInitial context:nil];

// NSKeyValueObservingOptionPrior
// [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionPrior context:nil];

// NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
[person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionNew context:@"idbeny"];

person.age = 20;

// 被观察对象的属性发生改变时自动调用该方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"keypath:%@ object:%@ change:%@ context:%@", keyPath, object, change, context);

/** kNilOptions
输出:
keypath:age
object:<YBPerson: 0x60000218a5e0>
change:{
kind = 1;
}
context:(null)
*/

/** NSKeyValueObservingOptionNew
输出:
keypath:age
object:<YBPerson: 0x600000c363c0>
change:{
kind = 1;
new = 20;
}
context:(null)
*/

/** NSKeyValueObservingOptionOld
输出:
keypath:age
object:<YBPerson: 0x600000daece0>
change:{
kind = 1;
old = 10;
}
context:(null)
*/

/** NSKeyValueObservingOptionInitial
输出:
keypath:age
object:<YBPerson: 0x600000ae4840>
change:{
kind = 1;
}
context:(null)
*/

/** NSKeyValueObservingOptionPrior
输出:
keypath:age
object:<YBPerson: 0x60000226a960>
change:{
kind = 1;
notificationIsPrior = 1; // 有此属性代表修改前调用,反之修改后调用
}
context:(null)

keypath:age
object:<YBPerson: 0x60000226a960>
change:{
kind = 1;
}
context:(null)
*/

/** NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
输出:
keypath:age
object:<YBPerson: 0x6000001659e0>
change:{
kind = 1;
new = 20;
old = 10;
}
context:idbeny
*/
}

上面案例可以看到各种options的输出,开发中最常用就是NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld。每次在不需要观察后,记得把观察者移除[self.person removeObserver:self forKeyPath:@"age"];,如果有context,则使用[self.person removeObserver:self forKeyPath:@"age" context:@"idbeny"];

KVO的本质就是在底层监听并执行了属性的setter方法

  • 值改变之前调用willChangeValueForKey:
  • 修改值;
  • 修改值后调用didChangeValueForKey:
  • 给观察对象发送消息observeValueForKeyPath:ofObject:change:context

在被观察者对象的实现文件里,重写willChangeValueForKeydidChangeValueForKey做拦截操作。