一、概述
KVO是基于观察者设计模式来实现的。
观察者模式:一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。
KVO:Key-Value Observing,是Foundation框架提供的一种机制,使用KVO,可以方便地对指定对象的某个属性进行观察,当属性发生变化时,进行通知。
使用KVO只需要两个步骤:
(1)注册Observer;
(2)接收通知。
二、KVO注册与接收通知
1、注册Observer
使用下面方法注册Observer:
|
addObserver
:
forKeyPath
:
options
:
context
:
|
其中三个参数的含义如下:
- observer:观察者,需要响应属性变化的对象。该对象必须实现 observeValueForKeyPath:ofObject:change:context: 方法。
- keyPath:要观察的属性名称。要和属性声明的名称一致。
- options:对KVO机制进行配置,修改KVO通知的时机以及通知的内容,在后面详解。
- context:接收一个C指针,指向希望监听的属性。如:&self->_testData
需要注意的是,注册Observer之后一定要在合适的机会解除注册,否则会引发资源泄露,取消注册的方法:
|
removeObserver
:
forKeyPath
:
context
:
|
参数含义同注册时方法的参数含义。
options参数:
enum类型,在注册时传入,共有四种取值方式:
|
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的配置确定。
2、接收通知
当属性的值发生变化时,框架默认会自动通知注册的观察者。
上文提到,观察者需要实现方法:
|
-
(
void
)
observeValueForKeyPath
:
(
NSString *
)
keyPath
ofObject
:
(
id
)
object
change
:
(
NSDictionary *
)
change
context
:
(
void
*
)
context
|
这个方法就是接收通知的方法。参数含义如下:
- object:这个是所监听的对象,也就是所监听的属性所属的对象。
- change:是传入的变化量,通过在注册时用options参数进行的配置,会包含不同的内容。
- 其他参数含义同注册时方法的参数含义。
在实现这个方法中需要注意的是, 一定要对注册监听的所有属性都进行处理——使用context参数进行判断——否则Xcode会警告。
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实现监听Book的price改变:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//
// Book.h
// KVOTest
//
// Created by 李峰峰 on 2017/1/17.
// Copyright © 2017年 李峰峰. All rights reserved.
//
#import <Foundation/Foundation.h>
@
interface
Book
:
NSObject
@
property
(
nonatomic
,
strong
)
NSString *
name
;
@
property
(
nonatomic
,
strong
)
NSString *
price
;
@
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
|
//
// ViewController.m
// KVOTest
//
// Created by 李峰峰 on 2017/1/17.
// Copyright © 2017年 李峰峰. All rights reserved.
//
#import "ViewController.h"
#import "Book.h"
@
interface
ViewController
(
)
@
property
(
nonatomic
,
strong
)
Book *
abook
;
@
end
@
implementation
ViewController
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
[
self
addObserver
]
;
[
self
addBtn
]
;
}
/**
添加监听
*/
-
(
void
)
addObserver
{
//添加监听
self
.
abook
=
[
[
Book
alloc
]
init
]
;
self
.
abook
.
price
=
@
"0"
;
//先设一个初始值
[
_abook
addObserver
:
self
forKeyPath
:
@
"price"
options
:
NSKeyValueObservingOptionOld
|
NSKeyValueObservingOptionNew
context
:
nil
]
;
}
/**
添加一个按钮
*/
-
(
void
)
addBtn
{
UIButton *
abtn
=
[
UIButton
buttonWithType
:
UIButtonTypeCustom
]
;
abtn
.
frame
=
CGRectMake
(
80
,
90.0
,
80
,
30
)
;
[
abtn
setTitleColor
:
[
UIColor
blueColor
]
forState
:
UIControlStateNormal
]
;
[
abtn
setTitle
:
@
"Change"
forState
:
UIControlStateNormal
]
;
[
abtn
addTarget
:
self
action
:
@
selector
(
btnClick
)
forControlEvents
:
UIControlEventTouchUpInside
]
;
[
self
.
view
addSubview
:
abtn
]
;
}
/**
按钮点击事件
*/
-
(
void
)
btnClick
{
NSLog
(
@
"点击了Btn!"
)
;
NSInteger
randomPrice
=
arc4random
(
)
%
100
;
NSString *
newPrice
=
[
NSString
stringWithFormat
:
@
"%ld"
,
(
long
)
randomPrice
]
;
//两种方法触发监听
//第一种方法
// NSDictionary *newBookPropertiesDictionary=[NSDictionary dictionaryWithObjectsAndKeys:
// @"book name",@"name",
// newPrice,@"price",nil];
// [self.abook setValuesForKeysWithDictionary:newBookPropertiesDictionary];
//第二种方法
[
self
.
abook
setValue
:
newPrice
forKey
:
@
"price"
]
;
}
//实现监听
-
(
void
)
observeValueForKeyPath
:
(
NSString *
)
keyPath
ofObject
:
(
id
)
object
change
:
(
NSDictionary *
)
change
context
:
(
void
*
)
context
{
if
(
[
keyPath
isEqual
:
@
"price"
]
)
{
NSLog
(
@
"old price: %@"
,
[
change
objectForKey
:
@
"old"
]
)
;
NSLog
(
@
"new price: %@"
,
[
change
objectForKey
:
@
"new"
]
)
;
}
}
-
(
void
)
dealloc
{
//移除监听
[
_abook
removeObserver
:
self
forKeyPath
:
@
"price"
]
;
}
@
end
|
打印结果:
|
2017
-
01
-
17
16
:
16
:
03.633
KVOTest
[
40935
:
3327447
]
点击了
Btn
!
2017
-
01
-
17
16
:
16
:
03.634
KVOTest
[
40935
:
3327447
]
old
price
:
0
2017
-
01
-
17
16
:
16
:
03.634
KVOTest
[
40935
:
3327447
]
new
price
:
87
2017
-
01
-
17
16
:
16
:
04.944
KVOTest
[
40935
:
3327447
]
点击了
Btn
!
2017
-
01
-
17
16
:
16
:
04.945
KVOTest
[
40935
:
3327447
]
old
price
:
87
2017
-
01
-
17
16
:
16
:
04.945
KVOTest
[
40935
:
3327447
]
new
price
:
49
|
代码很简单,不再做过多解释。
3、自动通知和手动通知
上面提到,KVO默认会自动通知观察者。取消自动通知的方法是实现:
|
+
(
BOOL
)
automaticallyNotifiesObserversForKey
:
(
NSString *
)
key
|
方法,通过返回NO来控制取消自动通知。
针对非自动通知的属性,可以分别在变化之前和之后手动调用如下方法(will在前,did在后)来手动通知观察者:
- (will/did)ChangeValueForKey:
- (will/did)ChangeValueForKey:withSetMutation:usingObjects:
- (will/did)Change:valuesAtIndexes:forKey:
手动通知的好处就是,可以灵活加上自己想要的判断条件,事实上自动通知也是框架通过调用这些方法实现的。
例如:
被观察的对象Target(重写setter/getter方法):
Target.h:
|
@
interface
Target
:
NSObject
{
int
age
;
}
// for manual KVO
-
age
-
(
int
)
age
;
-
(
void
)
setAge
:
(
int
)
theAge
;
@
end
|
Target.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
|
@
implementation
Target
-
(
id
)
init
{
self
=
[
super
init
]
;
if
(
nil
!=
self
)
{
age
=
10
;
}
return
self
;
}
// for manual KVO - age
-
(
int
)
age
{
return
age
;
}
-
(
void
)
setAge
:
(
int
)
theAge
{
[
self
willChangeValueForKey
:
@
"age"
]
;
age
=
theAge
;
[
self
didChangeValueForKey
:
@
"age"
]
;
}
+
(
BOOL
)
automaticallyNotifiesObserversForKey
:
(
NSString *
)
key
{
if
(
[
key
isEqualToString
:
@
"age"
]
)
{
return
NO
;
}
return
[
super
automaticallyNotifiesObserversForKey
:
key
]
*
*
;
}
@
end
|
三、KVO实现原理
KVO的实现是基于runtime运行时的,下面就来详细介绍一下原理,如下图:
- 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
- 派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
- 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
四、KVO 和线程
一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange...
会触发 KVO 通知。
所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO。
KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO 会保证下列两种情况的发生:
首先,如果我们调用一个支持 KVO 的 setter 方法,如下所示:
|
self
.
exchangeRate
=
2.345
;
|
KVO 能保证所有 exchangeRate
的观察者在 setter 方法返回前被通知到。
其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior
选项,直到 -observe...
被调用之前, exchangeRate
的 accessor 方法都会返回同样的值。
五、常见的 KVO 错误
首先,KVO 兼容是 API 的一部分。如果类的所有者不保证某个属性兼容 KVO,我们就不能保证 KVO 正常工作。苹果文档里有 KVO 兼容属性的文档。例如,NSProgress
类的大多数属性都是兼容 KVO 的。
当做出改变以后,有些人试着放空的 -willChange
和 -didChange
方法来强制 KVO 的触发。KVO 通知虽然会生效,但是这样做破坏了有依赖于 NSKeyValueObservingOld
选项的观察者。详细来说,这影响了 KVO 对观察键路径 (key path) 的原生支持。KVO 在观察键路径 (key path) 时依赖于 NSKeyValueObservingOld
属性。
我们也要指出有些集合是不能被观察的。KVO 旨在观察关系 (relationship) 而不是集合。我们不能观察 NSArray
,我们只能观察一个对象的属性——而这个属性有可能是 NSArray
。举例说,如果我们有一个 ContactList
对象,我们可以观察它的 contacts
属性。但是我们不能向要观察对象的 -addObserver:forKeyPath:...
传入一个 NSArray
。
相似地,观察 self
不是永远都生效的。而且这不是一个好的设计。
六、调试 KVO
你可以在 lldb
里查看一个被观察对象的所有观察信息。
|
(
lldb
)
po
[
observedObject
observationInfo
]
|
这会打印出有关谁观察谁之类的很多信息。
这个信息的格式不是公开的,我们不能让任何东西依赖它,因为苹果随时都可以改变它。不过这是一个很强大的排错工具。