本文源自https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.pdf
KVO是一种允许指定的对象在一些其他对象指定的属性改变时被通知的机制。
为了明白KVO,你首先需要明白KVC。
At a Glance
KVO是一种允许指定的对象在一些其他对象指定的属性改变时被通知的机制。它在你应用程序的模型和控制器交互时显得特别有用。一个控制器对象典型的观察模型的属性,视图对象通过控制器观察模型对象。并且,一个模型对象可能观察其他的模型对象或者观察它自己。
你可以观察简单的属性,一对一的关系,一对多的关系 。指定的类型发生改变时多个观察的对象都会被通知的,以及那些参与改变的对象。
有三步来对一个属性设置观察者。明白以下三步可以提供一个清晰的阐述KVO是如何工作的。
1.首先,看是否有一个情景来让KVO产生好处。比如,一个对象需要在另外一个对象指定的属性发生改变时被通知。
比如,一个PersonObject可能希望在BankObject中的accountBalance改变时被通知。
2. PersonObject必须注册成BankObject属性的accountBalance的观察者,通过发送
addObserver:forKeyPath:options:context:
消息。
[bankInstance addObserver:personInstance forKetPath:@“accountBalance” option:NSKeyValueObservingOptionNew context:NULL];
注意:这个方法建立了一个你指定对象之间的链接。链接仅仅是建立在指定的实例之间,没有建立类之间的链接
3. 为了响应更改的通知,观察者必须实现observeValueForKeyPath:ofObject:change:context:方法。这个方法的实现定义了观察者应该如何响应更改通知。这个方法你可以自定义你为一个观察属性的响应。
4. 方法
observeValueForKeyPath:ofObject:change:context是在观察的属性值发生改变时以KVO遵从的方式自动调用的。
KVO的主要优势是你不需要实现你自己的调度在属性变化时发送通知。它完美的定义的基础有一个框架层面的支持来简单的适应,不需要在你的项目中添加任何代码。并且,基础已经有比较丰富的特性,可以很简单的来对一个属性支持多个观察者,以及相关的值。
KVO Compliance描述了自动和手动KVO观察的不同并且如何实现它们。
根用NSNotificationCenter的通知不同的是,KVO没有中央对象为所有的观察者提供更改通知。相反,当更改发生时,通知被直接发送给这些观察者对象。NSObject提供了这个KVO基础的实现,你很少需要重写这样方法。
Registering for Key-Value Observing(注册为键值对观察)
为了接收一个属性的键值观察的通知,三件事情是需要做的:
1. 你希望观察的被观察的类的属性必须是键值观察兼容的。
2. 你必须用被观察的对象来注册观察者,使用方法
addObserver:forKeyPath:options:context
3. 被观察的类必须实现
observeValueForKeyPath:ofObject:change:context:
.
重点:不是所有的类的所有属性都是键值观察兼容的。你可以使用下列的步骤来确保你的类是否是键值观察兼容的。
注册成一个观察者
为了在一个属性改变时被通知,一个观察者对象必须首先在被观者者对象发送addObserver:forKeyPath:options:context消息注册成观察对象。这个方法的options参数指出的信息是改变的通知什么时候被发送的。使用NSKeyValueObservingOptionOld选项指定原始对象的值当作更改字典中的一个实体提供给观察者。指定NSKeyValueObservingNew选项把新值当作更改字典中的一个实体提供给观察者对象。为了同时接收2个值,你可以对option常量使用或运算。
下面的例子阐述了为openingBalance属性注册一个观察者对象
- (void)registerAsObserver
{
[account addObserver:inspector forKeyPath:@“openingBalance” options:(NSKeyValueObservingNew | NSKeyValueObservingOld) context:NULL];
}
当你注册一个对象成为观察者时,你可以提供一个context指针。这个指针在
observeValueForKeyPath:ofObject:change:context:调用时传递给观察者。这个指针可以是一个C指针或者是一个对象饮用。这个指针可以用来当作唯一的标识来决定更改是否需要被观察,或者提供一些额外的信息给观察者对象。
注意:KVO
observeValueForKeyPath:ofObject:change:context:方法并不对观察对象,被观察对象或者context保持强引用。你必须在必要时确保你对这些对象保持强引用。
接收更改的通知
当被观察的属性的值发生更改时,观察者接收一个
observeValueForKeyPath:ofObject:change:context:消息。所有的观察者必须实现这个方法。
当观察的通知被触发时,
提供
观察者一个对象和key的路径,一个包含更改细节的字典,一个在观察者注册时提供的上下文指针。
更改字典实体NSKeyValueChangeKindKey提供了关于发生更改的类别的信息。如果观察对象的值发生改变时,NSKeyValueChangeKindKey实体返回NSKeyValueChangeSetting。取决于观察者注册时指定的options,NSKeyValueChangeOldKey和NSKeyValueChangeNewKey实体在更改字典中包含了属性更改前和更改后的值。如果属性是一个对象,值就被直接提供了。如果属性是标量或者C结构体,值是以NSValue对象包装了。
如果被观察的属性是一对多的关系,这个NSKeyValueChangeKindKey实体同样指出对象在关系中是否被插入,删除或者取代
分别
通过NSKeyValueChangeInsertion,NSKeyValueChangeRemoval,NSKeyValueChangeReplacement。
更改词典为NSKeyValueChangeIndexesKey的实体是一个NSIndexSet对象指定关系中发生改变的索引。如果
NSKeyValueChangeOldKey或
NSKeyValueChangeNewKey在注册时被指定,更改字典中是包含相关对象改前改后的值的数组。
下面这个例子展示了inspector的
observeValueForKeyPath:ofObject:change:context:的实现影响旧的和新的openingBalance属性的值
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqual:@"openingBalance"]) {
[openingBalanceInspectorField setObjectValue:
[change objectForKey:NSKeyValueChangeNewKey]];
} /*
Be sure to call the superclass's implementation *if it implements it*.
NSObject does not implement the method.
*/
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
删除一个注册对象
被观察对象通过发送
removeObserver:forKeyPath:消息来移除一个观察者。如下所示:
- (void)unregisterForChangeNotification {
[observedObject removeObserver:inspector forKeyPath:@"openingBalance”];
}
如果context是一个对象,你必须保持一个对它的强引用直到移除这个观察者。接收
removeObserver:forKeyPath:消息以后,观察者对象再也不会从指定的对象和键路径接收任何
observeValueForKeyPath:ofObject:change:context:消息了。
KVO Compliance
为了让一个指定的属性被认为是一个遵从KVO的,一个类必须确信以下几点:
1. 类的这个属性必须是符合KVC
2. 类为这个属性发出KVO更改通告。
3. 依赖的键被正确的注册了。
有两种机制来保证更改通告被发出了。NSObject提供的自动支持是默认的,为类的所有符合KVC的属性提供自动支持。典型的,如果你支持标准的Cocoa 编码和命名约定,你可以使用自动更改通告,你不用写额外的代码。
手动更改通告提供了额外的控制,什么时候通知被发送,需要额外的编码。通过实现类方法
automaticallyNotifiesObserversForKey:你可以控制你子类的属性的自动通告。
自动更改通告(Automatic Change Notification)
NSObject为自动键值更改通告提供了基本的实现。自动键值更改通告使用key-value compliant accessor 通知观察者更改发生了,以及KVC方法。自动通知同时支持通过返回的代理对象的集合,比如mutableArrayValueForKey。
下面的例子展示了属性name的更改导致的通知:
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
手动更改通告(Manual Change Notification)
手动更改通告提供了更加细颗粒的控制:能控制如何,何时消息被发送给观察者。在帮助减少不必要的触发通知是有用的,或者把一组更改转到一个通知。
一个类实现手动通知的话必须实现NSObject实现的
automaticallyNotifiesObserversForKey:方法。在同一个类中同时使用自动和手动观察通知是可以的。执行手动通知的对象,子类实现的
automaticallyNotifiesObserversForKey:
必须返回NO。子类实现的时候必须调用父类的这个方法来处理不认识的keys。以下的例子允许对属性openingBalance属性进行手动通知,允许父类来决定其他的keys的通知。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
为了实现手动观察通知,你调用willChangeValueForKey在更改这个值之前,并且调用didChangeValueForKey在更改这个值之后。如下所示:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance”];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance”];
}
通过首先检查值是否发生改变,你可以最小化不必要的消息发送。如下:
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != openingBalance) {
[self willChangeValueForKey:@"openingBalance”];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance”];
}
}
如果一个操作引起多个键发生更改,你必须嵌套进行更改通告:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
在一个有序的一对多的情况下,你必须指定发生改变的值,同时需要指定发生改变的类别和涉及到的对象的索引。更改的类型是NSKeyValueChange,包括NSKeyValueChangeInsertion,NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement。受影响对象的索引是以NSIndexSet对象传递的。
下面的代码片段阐述了在一对多的关系事物中如何包装一个删除对象。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
注册依赖的Keys
有许多情况是当属性的值依赖于其他对象的一个或者多个其他的属性。如果一个属性的值发生了改变,依赖的属性的值也必须被改变。你如何确定KVO通知被发送给这些依赖的属性依赖于这些关系集。
一对一的关系
为了自动触发通知给一个一对一的关系,你要么重写
keyPathsForValuesAffectingValueForKey:或者实现一个合适的方法遵从它定义的依赖keys的方式。
比如,fullName同时依赖与first和last names。方法返回full name需要按下面的方式编写:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
一个应用观察fullName属性时,必须在firstName或者lastName属性被更改时被通知,就像fullName被影响了一样。
一种解决方案是重写
keyPathsForValuesAffectingValueForKey:指定fullName属性依赖于属性firstName和lastName。如下展示了这样的一个依赖:
+ (
NSSet
*)keyPathsForValuesAffectingValueForKey:(
NSString
*)key {
NSSet
*keyPaths = [
super
keyPathsForValuesAffectingValueForKey
:key];
if
([key
isEqualToString
:
@"fullName"
]) {
NSArray *affectingKeys =
@[
@"lastName"
,
@"firstName"
]
;
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return
keyPaths;
}
你的重写必须调用父类的方法并且返回包含依赖的keys的set。
你可以达到同样的结果实现一个类方法遵从命名规则
keyPathsForValuesAffecting
,key 就是属性的名字,这里是fullName。使用这种方式重写
keyPathsForValuesAffecting
FullName方法:
+ (
NSSet
*)keyPathsForValuesAffectingFullName {
return
[
NSSet
setWithObjects
:
@"lastName"
,
@"firstName"
,
nil
];
}
注意:当你使用category扩展一个已经存在的类,在category中添加一个计算属性时,你不能重写
keyPathsForValuesAffectingValueForKey方法。相反,
您必须遵守每个对象的相应属性的一对多收集和自行更新依赖键响应改变他们的价值观。下面的部分显示了一个策略来处理这种情况。
一对多的关系
keyPathsForValuesAffectingValueForKey方法不
包含
支持
一对多的
键路径。比如,假设你有一个Department对象与employee拥有一对多的关系(employees)。Employee有一个salary属性,你的Department有一个totalSalary的属性依赖于关系中所有employee的salary。你不能用
keyPathsForValuesAffectingTotalSalary,返回employees.salary的key。
为这两种情形有2种可能的解决方法:
1. 你可以使用KVO注册parent(Department)成为每个孩子(Employee)的相关属性的观察者。你必须添加和删除父类为一个孩子对象的观察者来添加和删除关系。在
observeValueForKeyPath:ofObject:change:context:方法中,你更新依赖的值来响应更改,如下面代码片段所展示:
- (
void
)observeValueForKeyPath:(
NSString
*)keyPath ofObject:(
id
)object
change:(
NSDictionary
*)change context:(
void
*)context {
if
(context == totalSalaryContext) {
[
self
updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (
void
)
updateTotalSalary
{
[
self
setTotalSalary:[
self
valueForKeyPath:
@"[email protected]"
]];
}
- (
void
)setTotalSalary:(NSNumber *)newTotalSalary {
if
(totalSalary != newTotalSalary) {
[
self
willChangeValueForKey:
@"totalSalary"
];
_totalSalary = newTotalSalary;
[
self
didChangeValueForKey:
@"totalSalary"
];
}
}
- (NSNumber *)totalSalary {
return
_totalSalary;
}
2.如果你使用Core Data,你可以用应用程序的通知中心注册这个parent为它管理对象的context的一个观察者。parent必须响应孩子发送的相关的更改通知,类似于KVO的方式。
KVO实现的细节
自动KVO是通过一个叫做isa-swizzling的技术实现的。
isa指针,就像它名字建议的那样,指像拥有dispatch table的对象的类。这个dipatch table实质上包括类实现的方法的指针。
当一个观察者为一个对象的属性注册时,被观察对象的isa指针被改变了,指向了中间类而不是指向真正的类。结果就是isa指针的值不会影响真正类的实例。
你永远也不应该依赖isa指针来决定类关系。相反,你必须使用类方法来决定对象实例的类。