iOS学习笔记——KVO

KVO:Key-Value Observing,是Foundation框架提供的一种机制,使用KVO,可以方便地对指定对象的某个属性进行观察,当属性发生变化时,进行通知。

使用KVO只需要两个步骤:

  1. 注册Observer;
  2. 接收通知。

注册Observer

使用addObserver:forKeyPath:options:context:消息注册Observer,其中三个参数的含义如下:

  • observer:观察者,需要响应属性变化的对象。该对象必须实现 observeValueForKeyPath:ofObject:change:context: 方法。
  • keyPath:要观察的属性名称。要和属性声明的名称一致。
  • options:对KVO机制进行配置,修改KVO通知的时机以及通知的内容,在后面详解。
  • context:接收一个C指针,指向希望监听的属性。如:&self->_testData
    需要注意的是,注册Observer之后一定要在合适的机会解除注册,否则会引发资源泄露,取消注册的方法:
1
removeObserver:forKeyPath:context:

参数含义同注册时方法的参数含义。

接收通知

当属性的值发生变化时,框架默认会自动通知注册的观察者。

上文提到,观察者需要实现方法:

1
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

这个方法就是接收通知的方法。参数含义如下:

  • object:这个是所监听的对象,也就是所监听的属性所属的对象。
  • change:是传入的变化量,通过在注册时用options参数进行的配置,会包含不同的内容。
  • 其他参数含义同注册时方法的参数含义。
    在实现这个方法中需要注意的是, 一定要对注册监听的所有属性都进行处理——使用context参数进行判断——否则Xcode会警告。

options参数

enum类型,在注册时传入,共有四种取值方式:

1
2
3
4
5
6
7
enum {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial = 0x04,
NSKeyValueObservingOptionPrior = 0x08
};
typedef NSUInteger NSKeyValueObservingOptions;

四个值的含义如下:

  • NSKeyValueObservingOptionNew:接收方法中使用change参数传入变化后的新值,键为:NSKeyValueChangeNewKey;
  • NSKeyValueObservingOptionOld:接收方法中使用change参数传入变化前的旧值,键为:NSKeyValueChangeOldKey;
  • NSKeyValueObservingOptionInitial:注册之后立刻调用接收方法,如果配置了NSKeyValueObservingOptionNew,change参数内容会包含新值,键为:NSKeyValueChangeNewKey;
  • NSKeyValueObservingOptionPrior:如果加入这个参数,接收方法会在变化前后分别调用一次,共两次,变化前的通知change参数包含notificationIsPrior = 1。其他内容根据NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld的配置确定。

change参数

除了根据options参数控制的change参数内容,默认change参数会包含一个NSKeyValueChangeKindKey键值对,传递被监听属性的变化类型:

enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

含义很直白:

  • NSKeyValueChangeSetting:属性的值被重新设置;
  • NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement:表示更改的是集合属性,分别代表插入、删除、替换操作。
    如果NSKeyValueChangeKindKey参数是针对集合属性的三个之一,change参数还会包含一个NSKeyValueChangeIndexesKey键值对,表示变化的index。

自动通知和手动通知

上文提到,KVO默认会自动通知观察者。取消自动通知的方法是实现

1
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key

方法,通过返回NO来控制取消自动通知。

针对非自动通知的属性,可以分别在变化之前和之后手动调用如下方法(will在前,did在后)来手动通知观察者:

  • (will/did)ChangeValueForKey:
  • (will/did)ChangeValueForKey:withSetMutation:usingObjects:
  • (will/did)Change:valuesAtIndexes:forKey:

因为大多情况下都使用自动通知,这里就不过多解释。事实上自动通知也是框架通过调用这些方法实现的。

Demo

下面这个例子演示了KVO的使用,供大家参考。运行效果:

  • 开始运行效果,因为testData配置了NSKeyValueObservingOptionInitial,所以立刻调用
    屏幕快照 2014-10-20 下午3.22.31

  • 第一次和第二次更改属性后的输出。
    屏幕快照 2014-10-20 下午3.22.35

屏幕快照 2014-10-20 下午3.22.41

Code:

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
#import "ViewController.h"
@interface ViewController ()

@property (assign, nonatomic) NSString *testData;
@property (assign, nonatomic, readwrite) BOOL testBoolean;
@property (weak, nonatomic) IBOutlet UITextView *resultText;

@end
@implementation ViewController

#pragma mark - life-cycle

- (void)viewDidLoad
{
[super viewDidLoad];
[self addObserver:self forKeyPath:@"testData"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior
context:&self->_testData];
[self addObserver:self forKeyPath:@"testBoolean" options:0 context:&self->_testBoolean];
}

- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self removeObserver:self forKeyPath:@"testData" context:&self->_testData];
[self removeObserver:self forKeyPath:@"testBoolean"context:&self->_testBoolean];
}

#pragma mark - action

- (IBAction)changeData:(UIButton *)sender
{
static int times = 0;
times ++;
[self appendToResult: [NSString stringWithFormat: @"--- click %i ---n", times]];
if (!_testData) {
self.testData = @"newData";
} else {
self.testData = @"newData2";
}
self.testBoolean = YES;
}

#pragma mark - Observing

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == &self->_testData) {
assert([NSThread isMainThread]);
[self appendToResult: [NSString stringWithFormat:@"--- testData ---noptions: %@n", change]];
} else if (context == &self->_testBoolean) {
assert([NSThread isMainThread]);
[self appendToResult: [NSString stringWithFormat:@"--- testBoolean ---noptions: %@n", change]];
[self appendToResult: [NSString stringWithFormat:@"new boolean: %in", self.testBoolean]];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
[self appendToResult: @"--- end ---nn"];
}

- (void)appendToResult: (NSString *) result
{
self.resultText.text = [self.resultText.text stringByAppendingString: result];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"testBoolean"]) {
//        返回NO会使得不能接受testBoolean的通知
return YES;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end