版本记录
版本号 | 时间 |
---|---|
V1.0 | 2017.09.14 |
前言
KVO
具有更强大的功能,是苹果给我们的一个回调机制,在某个对象注册监听者后,在被监听的对象发生改变时,对象会发送一个通知给监听者,以便监听者执行回调操作。接下来几篇就详细的解析一下KVO。感兴趣的可以看上面几篇。
1. KVO解析(一) —— 基本了解
键值观察步骤
为确保对象可以接受键值观察的通知,必须执行如下步骤:
为被观察的对象注册观察者,使用方法addObserver:forKeyPath:options:context:
实现方法observeValueForKeyPath:ofObject:change:context:,在观察者的内部接收改变的通知消息。
当不再需要接收消息的时候,使用方法removeObserver:forKeyPath:移除通知,还有一点,就是要在观察者从内存释放之前调用此方法。
还要注意:并非所有类都符合所有属性的KVO。 您可以按照KVO合规性中描述的步骤确保您自己的类符合KVO。 通常,如果Apple提供的框架中的属性通常被记录,那么它们符合KVO的合规性。
注册观察者
观察对象首先通过发送一个addObserver:forKeyPath:options:context:
消息来注册观察对象,将自身传递给观察者和要观察的属性的关键路径。 观察者另外指定一个选项参数option
和一个上下文指针context
来管理通知的各个方面。
1. Options
指定为选项常量的按位OR的options参数影响通知中提供的change
字典的内容以及生成通知的方式。
您选择通过指定选项NSKeyValueObservingOptionOld
从更改之前接收观察到的属性的值。 您使用选项NSKeyValueObservingOptionNew
请求属性的新值。 您可以使用这些选项的按位OR来收到旧值和新值。
您指示被观察的对象发送立即更改通知(在addObserver:forKeyPath:options:context:returns
之前),其选项为NSKeyValueObservingOptionInitial
。 您可以使用此额外的一次性通知来建立观察者中的属性的初始值。
您可以通过包含NSKeyValueObservingOptionPrior
选项来指示观察对象在属性更改之前发送通知(除更改之后的通常通知之外)。 change
更改字典表示通过将NSKeyValueChangeNotificationIsPriorKey
的关键字与NSNumber
包装YES的值相关联的代替通知。 那个key是不存在的。 当观察者自己的KVO合规性要求它调用一个取决于所观察属性的其中一个属性的-willChange ...
方法时,可以使用预置通知。 通常的修改后通知太迟了,无法及时调用willChange ...
。
2. Context
addObserver:forKeyPath:options:context:
方法中的上下文指针包含将在相应的更改通知中传回给观察者的任意数据。 您可以指定NULL
并完全依赖于键路径字符串来确定更改通知的来源,但是由于不同的原因,此方法可能会导致超类也遵循相同键路径的对象的问题。
更安全和更可扩展的方法是使用上下文来确保您收到的通知注定给您的观察者,而不是超类。
你的类中唯一命名的静态变量的地址是一个很好的上下文。 在超类或子类中以类似方式选择的上下文将不太可能重叠。 您可以为整个类选择单个上下文,并依靠通知消息中的键路径字符串来确定更改的内容。 或者,您可以为每个观察到的键路径创建一个独特的上下文,这样可以避免完全需要字符串比较,从而实现更有效的通知解析。 下面显示了以这种方式选择的balance
和interestRate
属性的示例上下文。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
下面的例子,展示的是一个Person对象是如何利用给定的上下文指针注册自己作为Account
对象balance
和interestRate
属性的观察者的。
- (void)registerAsObserverForAccount:(Account*)account
{
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
这里还要注意:键值观察addObserver:forKeyPath:options:context:
方法不保持对观察对象,被观察对象或上下文的强引用。如果需要的话, 您应该确保在必要时保持对观察,被观察,对象和上下文的强烈引用。
接收Change的通知
当被观察对象属性的值发生变化时,观察者会接收到observeValueForKeyPath:ofObject:change:context:
消息。 所有观察者必须实现这种方法。
被观察对象提供了键值路径并触发通知,并把自己作为关联对象,还有关于改变的详细内容的字典、当观察者注册此键值路径时提供的上下文指针。
更改字典条目NSKeyValueChangeKindKey
提供有关发生的更改类型的信息。 如果观察到的对象的值已更改,NSKeyValueChangeKindKey
条目将返回NSKeyValueChangeSetting
。 根据注册观察者时指定的选项,更改字典中的NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
条目包含更改前后的属性值。 如果属性是对象,则直接提供该值。 如果属性是标量或C结构体,则该值将包装在NSValue
对象中(与键值编码KVC一样)。
如果观察到的属性是一对多关系,NSKeyValueChangeKindKey
条目还指示关系中的对象是否分别插入,删除或替换,分别返回NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
。
下面展示的是Person观察者实现的方法observeValueForKeyPath:ofObject:change:context:
,并输出balance
和interestRate
属性代表的新旧值。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
}
else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
}
else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
当你注册观察者时指定的上下文是NULL
时,您将通知的键路径与您观察到的键路径进行比较,以确定发生了什么变化。 如果您对所有观察到的键路径使用单个上下文,则首先根据通知的上下文测试,并找到匹配项,使用键路径字符串比较来确定特定的更改。 如果您为每个键路径提供了唯一的上下文,如下所示,一系列简单的指针比较可以同时告知您该通知是否适用于此观察者,如果是,则键路径已更改。
在任何情况下,观察者应该总是调用超类的实现observeValueForKeyPath:ofObject:change:context:
当它不识别上下文(或在简单的情况下,任何键路径)时,因为这意味着超类也已经注册通知。
注意:如果通知传播到类层次结构的顶部,NSObject
将抛出一个NSInternalInconsistencyException
,因为这是一个编程错误:一个子类不能使用它注册的通知。
观察者的移除
通过向被观察对象发送消息removeObserver:forKeyPath:context:
,指定观察对象、键路径和上下文,从而移除键值观察对象,下面列出的是Person对象移除自己作为balance
和interestRate
的观察者。
- (void)unregisterAsObserverForAccount:(Account*)account
{
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
接收到消息removeObserver:forKeyPath:context:
之后,观察者不再收到任何指定的键和对象的observeValueForKeyPath:ofObject:change:context:
消息。
当移除观察者时,还需要谨记几个问题:
如果你在移除观察者时,这个观察者尚未注册,就会导致
NSRangeException
错误,相对于方法addObserver:forKeyPath:options:context:
,你可以调用方法removeObserver:forKeyPath:context:
一次,或者如果这个在你的app里面是不可行的,那么就在try/catch block
里面处理这个潜在的错误和例外。当
dellocated
自己时,观察者不会自动移除自己。 观察到的对象继续发送通知,忽视观察者的状态。 但是,像发送给已释放对象的任何其他消息一样,更改通知会触发内存访问异常。 因此,您可以确保观察者在消失之前消除自己。如果该对象是观察者或观察者,则该协议无法询问对象。 构建您的代码以避免出现相关的错误。 典型的模式是在观察者初始化期间(例如在
init
或viewDidLoad
中)注册为观察者,并在释放(通常在dealloc
中)时注销,确保正确配对和有序添加和删除消息,并且观察者要在自己在内存中释放之前移除自己作为其他属性或对象的观察者。
后记
未完,待续~~~