目录
1. Key Value Observing 介绍
Observer Pattern 是”GOF”提到的24种面向对象设计模式中的一种, 它可以让subject(被观察者)的iVar改变时通知observer(观察者)。Key Value Observing 是Cocoa对于Observing Pattern的具体实现。 这在Model-View-Controller模式里提供了一种在Controller里,Model和View各自变化实时更新彼此的实现。 KVO实现的原理是subject拥有观察者们的引用(array),在iVar的setter的更改值前后加入通知观察者。令人疑惑的是Cocoa的KVO的API在没有重写subject的setter的同时又实现了通知观察者,这里用到了isa swizzling的方法,我们暂时不讲。本文着重介绍Key Value Observing的API应用, 包括自动观察,手动观察,依赖值观察,以及KVO的各种坑及应对方法。
2. Key Value Observing 三种应用
Key Value Observing 的应用逻辑遵循 ”订阅“-”响应“-”取消订阅“的基本方式。
2.1. 自动观察
先来看一段代码。
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
Bank.h
@class Person;
@interface Bank : NSObject
@property (nonatomic, strong) NSNumber* accountDomestic;
@property (nonatomic, strong) Person* person;
-(instancetype)initWithPerson:(Person *)person;
@end
Bank.m
@implementation Bank
-(instancetype)initWithPerson:(Person *)person{
if(self = [super init]){
_person = person;
[self addObserver:_person forKeyPath:NSStringFromSelector(@selector(accountDomestic)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:bankContext];
}
return self;
}
-(void)dealloc{
[self removeObserver:_person forKeyPath:NSStringFromSelector(@selector(accountDomestic))];
}
@end
Person.h
@interface Person : NSObject
@end
Person.m
@implementation Person
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if([object isKindOfClass:[Bank class]]){
if([keyPath isEqualToString:NSStringFromSelector(@selector(accountDomestic))]){
NSString *oldValue = change[NSKeyValueChangeOldKey];
NSString *newValue = change[NSKeyValueChangeNewKey];
NSLog(@"--------------------------");
NSLog(@"accountDomestic old value: %@", oldValue);
NSLog(@"accountDomestic new value: %@", newValue);
}
}
}
@end
ViewController.m
@interface ViewController ()
@property (nonatomic, strong) Bank *bank;
@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) NSNumber *delta;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.delta = @10;
self.bank = [[Bank alloc] initWithPerson:self.person];
}
- (IBAction)accountBalanceIncreaseButtonDidTouch:(id)sender {
self.bank.accountDomestic = self.delta;
int temp = [self.delta intValue];
temp += 10;
self.delta = [NSNumber numberWithInt:temp];
}
//输出:按两次button
//--------------------------
//accountDomestic old value: <null>
//accountDomestic new value: 10
//--------------------------
//accountDomestic old value: 10
//accountDomestic new value: 20
//
我们先来看”订阅”。这里Bank
类有一个iVar是accountDomestic
,还有一个客户Person
,在Bank
初始化时, Person
订阅了accountDomestic
的KVO服务, 被加入到观察者中。
1
2
3
4
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
这里observer
是person
,观察者;keyPath
是iVar的名字, 应该是@"accountDomestic"
,由于手动输入只能在运行时才能确认对错,用NSStringFromSelecotr(@selector(accountDomestic))
替代更为合适; option
是一个NSKeyValueObservingOptions
的enum,可以有四个值,一般用的是旧值NSKeyValueObservingOptionOld
和新值NSKeyValueObservingOptionNew
; context
通常是null也可以用作唯一的ID来区别这个类的观察和父类的观察,这个我们后面讲。
下面我们看”响应“。Person类实现了以下方法。
1
2
3
4
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString*, id> *)change
context:(void *)context;
keyPath
是Person
类添加的键,本例中是至少有一个为NSStringFromSelecotr(@selector(accountDomestic))
;object
是subject(被观察者),这里是Bank
实例。change
为一个NSDictionary
,当订阅时的option
是用的是旧值NSKeyValueObservingOptionOld
和新值NSKeyValueObservingOptionNew
时,可以分别通过change[NSKeyValueChangeOldKey]
和change[NSKeyValueChangeNewKey]
来访问;context
和订阅时的context
一样。
本例中我们首先判断响应是否来源于Bank,再判断是否是NSStringFromSelecotr(@selector(accountDomestic))
,然后再执行相关业务。由于KVO的响应不能像UIControl
的实例方法-addTarget:action:forControlEvents:
一样可以添加selector, 因此所有的响应都在-observeValueForKeyPath:ofObject:change:context:
中执行,这是KVO广为诟病的一大缺点之一。
最后我们来看取消订阅:
1
2
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
当subjectdealloc
时,我们取消订阅。
2.2. 手动观察
有时候观察者需要对被观察的某个键进或者几个键的通知发送进行控制, 比如Bank
还有一个属性accountForeign
, Person
同时也观察这个key
。但是现在Person
暂时想把accountForeign
改变时的发送通知给停止掉,或者暂停掉后又想开启。见如下代码。
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
Bank.m
@implementation Bank
-(instancetype)initWithPerson:(Person *)person{
if(self = [super init]){
_person = person;
[self addObserver:_person forKeyPath:NSStringFromSelector(@selector(accountDomestic)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:bankContext];
[self addObserver:_person forKeyPath:NSStringFromSelector(@selector(accountForeign)) options: NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:bankContext];
}
return self;
}
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
//当key时NSStringFromSelector(@selector(accountForeign))停止自动发送通知。
if([key isEqualToString:NSStringFromSelector(@selector(accountForeign))]){
return NO;
}else{
return YES;
}
}
-(void)dealloc{
[self removeObserver:_person forKeyPath:NSStringFromSelector(@selector(accountDomestic))];
[self removeObserver:_person forKeyPath:NSStringFromSelector(@selector(accountForeign))];
}
Person.m
@implementation Person
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if([object isKindOfClass:[Bank class]]){
if([keyPath isEqualToString:NSStringFromSelector(@selector(accountDomestic))]){
NSString *oldValue = change[NSKeyValueChangeOldKey];
NSString *newValue = change[NSKeyValueChangeNewKey];
NSLog(@"--------------------------");
NSLog(@"accountBalance old value: %@", oldValue);
NSLog(@"accountBalance new value: %@", newValue);
}else if([keyPath isEqualToString:NSStringFromSelector(@selector(accountForeign))]){
NSString *oldValue = change[NSKeyValueChangeOldKey];
NSString *newValue = change[NSKeyValueChangeNewKey];
NSLog(@"--------------------------");
NSLog(@"accountForeign old value: %@", oldValue);
NSLog(@"accountForeign new value: %@", newValue);
}
}
}
ViewController.m
...
@implementation ViewController
...
- (IBAction)accountBalanceIncreaseButtonDidTouch:(id)sender {
self.bank.accountDomestic = self.delta;
//同时更改accountForeign的值。
self.bank.accountForeign = self.delta;
int temp = [self.delta intValue];
temp += 10;
self.delta = [NSNumber numberWithInt:temp];
}
//输出:按两次button
//--------------------------
//accountDomestic old value: <null>
//accountDomestic new value: 10
//
@end
这里+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
对accountForeign
返回NO
,停止了它的值改变时自动发送通知,虽然它和accountDomestic
一样实现了”订阅“-”响应“-”取消订阅“。
如果要开启的话,要么更改+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
对accountForeign
返回YES
; 要么在Bank
中插入下列代码:
1
2
3
4
5
-(void)setAccountForeign:(NSNumber *)accountForeign{
[self willChangeValueForKey:NSStringFromSelector(@selector(accountForeign))];
_accountForeign = accountForeign;
[self didChangeValueForKey:NSStringFromSelector(@selector(accountForeign))];
}
- (void)willChangeValueForKey:(NSString *)key
和- (void)didChangeValueForKey:(NSString *)key
分别在setter里面值改变前后通知观察者。这时候在改变它的值得时候,就会收到通知继而触发响应了。
2.3. 依赖值观察
再比如,Bank
有一个totalAmount
iVar,它是accountDomestic
和accountForeign
的和, 我们希望accountDomestic
和accountForeign
任何一个的改变都会使得totalBalance也有一样KVO的功能。这就是所谓的依赖值观察。这里totalAmount
的”订阅”-“响应”-“取消订阅”的实现和accountDomestic
或者accountForeign
一样。唯一不同的是我们在Bank
里要加入如下代码:
1
2
3
4
5
6
7
8
//重写totalAmount的getter,体现其数值关系。
-(NSNumber *)totalAmount{
return [NSNumber numberWithInt:([self.accountDomestic intValue] + [self.accountForeign intValue])];
}
//keyPathsForValuesAffectingTotalAmount里返回两个依赖的key作为NSSet。
+(NSSet *)keyPathsForValuesAffectingTotalAmount{
return [NSSet setWithObjects:NSStringFromSelector(@selector(accountDomestic)),NSStringFromSelector(@selector(accountForeign)), nil];
}
这样任何一个accountDomestic
或者accountForeign
的改变,都会发送totalAmount
改变的通知。
2.4. KeyPath注意事项
结合ReactiveCocoa
1
RACObserve(targe,keyPath);
self->percolates->N
若想观察self.percolates是否指向新的percolates对象,则RACObserve(self,percolates)
;若想观察self.percolates.N是否指向新的N对象,则RACObserve(self.percolates, N)
;
该情况在Algo Project中的PercolateImplementationViewController.m遇到过。
3. 坑及应对
KVO有一些坑需要注意, 否则有时候会产生莫名其妙的bug;
-
keyPath
最好不要用动态输入,如NSStringFromSelector(@selector(accountDomestic))
代替@"accountDomestic"
; -
context
用来区分本类和super类的通知,例如Person
继承自Client
(Client
还有子类是Company
),Client
类里已经实现了对Bank某个属性的观察,这个时候在Person
里实现响应的时候,应该做如下处理:
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
Person.m
//定义了一个静态常数指针,值为自身的地址,用来表示一个KVO的observer的token
static void *const kPersonConext = &kPersonConext;
@implementation Person
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
//首先判断是不是自身的KVO响应,
if(context == kPersonConext){
if([object isKindOfClass:[Bank class]]){
if([keyPath isEqualToString:NSStringFromSelector(@selector(accountDomestic))]){
NSString *oldValue = change[NSKeyValueChangeOldKey];
NSString *newValue = change[NSKeyValueChangeNewKey];
NSLog(@"--------------------------");
NSLog(@"accountBalance old value: %@", oldValue);
NSLog(@"accountBalance new value: %@", newValue);
}else if([keyPath isEqualToString:NSStringFromSelector(@selector(accountForeign))]){
NSString *oldValue = change[NSKeyValueChangeOldKey];
NSString *newValue = change[NSKeyValueChangeNewKey];
NSLog(@"--------------------------");
NSLog(@"accountForeign old value: %@", oldValue);
NSLog(@"accountForeign new value: %@", newValue);
}
}
}else{//不是则调用父类。
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
static void *const kPersonConext = &kPersonConext
用来作为本类observer的token,区分父类。
- 在”响应”里
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
,由于没有selector参数,因此所有的响应都要写在这个函数的实现里,逻辑比较混乱,要多注意。
4. 总结
KVO是Cocoa对于Observer Pattern的内置实现,需要我们通过”订阅”-”响应“-”取消订阅“来设置自动, 手动或者值依赖KVO。KVO的API有一些坑需要注意,比如keyPath的选择,context的用法, ”响应“的混乱逻辑等。对于KVO的isa swizzle实现原理,我们将在后面OC Runtime系列会详细介绍。