KVO
阅读一下官网关于键值观察(Key-Value Observing)的文档
介绍
KVO是一种运行将其他对象的指定属性的更改通知给对象的机制。
重要: 为了理解KVO,必须首先理解KVC。
快速了解
KVO是一种运行将其他对象的指定属性的更改通知给对象的机制。对于应用程序中的模型层与控制器层之间的通信特别有用(在OS X中,控制器层绑定技术很依赖与KVO)。控制器对象通常观察模型对象的属性,而视图对象通过控制器观察模型对象的属性。此外,模型对象可以观察其他模型对象(通常用于确定依赖值何时修改)甚至观察自身(也是确定依赖值何时修改)。
可以观察属性,包括简单属性、一对一、一对多关系。一对多关系的观察者被告知所做更改的类型以及更改涉及的对象。
要使用KVO,首先必须确保被观察的对象符合KVO。通常情况下,如果对象继承自NSObject并以常规的方式创建属性,则对象和属性将自动符合KVO标准。当然也可以手动实现。KVO合规性描述了自动和手动KVO之间的区别及如何实现两者。
常见流程是使用addObserver:forKeyPath:options:context:消息来指定键路径注册自己为观察者,然后观察者实现observeValueForKeyPath:ofObject:change:context: 方法来接收每次变更的通知,最后,观察者释放前必须调用removeObserver:forKeyPath:移除观察者 。
注册KVO描述了注册、接收、注销KVO通知的整个过程。
KVO的主要好处是无需自己实现方案就能在属性每次更改时发送通知。其定义明确的基础架构具有框架级别的支持,是其易于使用,通常无需在项目中添加任何代码。此外,基础架构已具备了完整的功能,这使得支持支持单个属性的多个观察者变的很容易,依赖值也一样。
注册依赖键说明了如何指定一个键的取值依赖于另一个键的值。
与使用NSNotificationCenter的通知,没有中央对象为所有的观察者提供变更通知。而是在进行更改时将通知直接发送给观察对象。NSObject提供了KVO的基本实现,故基本上不需要重新任何方法。
KVO实现细节描述了KVO是如何实现的。
注册KVO
为了使对象能接收到符合KVO规范属性的KVO通知,必须指定如下步骤:
- 使用addObserver:forKeyPath:options:context:方法向被观察对象注册观察者。
- 在观察者内部实现observeValueForKeyPath:ofObject:change:context:用以接收变更通知消息。
- 当不再需要接收通知时使用removeObserver:forKeyPath:来注销观察者。至少在观察者从内存释放之前调用此方法。
重要: 并非所有类的所有属性都符合KVO。可以按照KVO合规性中所述步骤来确保自己的类符合KVO规范。通常苹果提供的框架中属性被记录成这样则表示其符合KVO规范。
注册成为观察者
观察者对象首选通过向被观察者发送addObserver:forKeyPath:options:context:消息来注册自己,将自己作为观察者和需要观察的属性键路径一起传递。观察者额外指定一个options参数和context指针来管理通知的各个方面。
Options
options参数指定一个按位或的可选常量,会影响通知中提供的变更字典的内容和生成通知的方式。
可以通过指定选项NSKeyValueObservingOptionOld选择接收被观察者属性更改之前的值。使用NSKeyValueObservingOptionNew选项来请求属性的新值。也可以通过按位或这两个选项使旧值新值都接收。
用NSKeyValueObservingOptionInitial选项来指示被观察对象立即发送变更通知(在addObserver:forKeyPath:options:context:返回之前)。也可以在观察者属性值创建初始化时使用此设置创建一次性通知。
通过包含NSKeyValueObservingOptionPrior选项可以指示被观察对象在属性变更之前发送通知(除了更改之后的常规通知)。通过更改字典中包含键NSKeyValueChangeNotificationIsPriorKey值为NSNumber包装的YES来表示变更之前的通知。否则表示这个键为not。当观察者自身符合KVO要求其为其依赖于被观察的属性的属性调用 -willChange..方法,可以使用更改之前的通知。通常的变更发送通知来不及及时调用willChange…方法。
Context
addObserver:forKeyPath:options:context:消息中的上下文指针包含任意数据都将在相应的更改通知中传递回观察者。可以指定NULL并完全依靠键路径字符串来确定更改通知的来源,但是这种方式可能会使对象的父类由于不同的原因也观察到相同的键路径而导致问题。
一种更安全、易扩展的方式是使用上下文来确定收到的通知是发给观察者的,而不是超类。
类中唯一命名的静态变量的地址构成了良好的上下文。在父类或子类中以类似方式选择的上下文不太可能重叠。可以为整个类选择一个单独上下文,然后依靠通知消息中的键路径字符串来确定更改的内容。另外,可以为每个观察的键路径常见一个不同的上下文,从而可以更有效地进行通知解析。
注意: KVO addObserver:forKeyPath:options:context: 方法不维护对正在观察的对象、被观察的对象、上下文的强引用。应该确保自己必要时维护对观察中、被观察对象、上下文的强引用。
接收变更的通知
当对象的观察属性的值变化时,观察者会收到一条observeValueForKeyPath:ofObject:change:context:消息。所有的观察者都必须实现此方法。
观察对象提供键路径供触发通知使用,它本身是相对对象,字典包含变更的细节,以及在为该键路径注册观察者时提供的上下文指针。
变更字典条目NSKeyValueChangeKindKey提供关于发生的更改类型的信息。如果被观察对象的值变化了,则NSKeyValueChangeKindKey条目返回NSKeyValueChangeSetting值。根据注册为观察者时指定的选项,NSKeyValueChangeOldKey 和 NSKeyValueChangeNewKey条目包含变更前后的属性值。如果属性是一个对象,则值被直接提供。如果属性是一个标量或C结构体,则值被包装在一个NSValue对象中(与KVC一样)。
如果观察到的属性是一对多关系,则NSKeyValueChangeKindKey条目还表示是否对象通过返回NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement处于被插入、被移除、被替换关系中。
NSKeyValueChangeIndexesKey的更改字典条目是一个NSIndexSet对象指定已更改关系中的索引。如果NSKeyValueObservingOptionNew 或 NSKeyValueObservingOptionOld在对象注册时被指定为选项,则更改字典中NSKeyValueObservingOptionNew 或 NSKeyValueObservingOptionOld条目是包含相关对象变更前后值的数组。
如果注册观察者时指定了NULL上下文,则将通知的键路径与要观察的键路径相比较以确定更改了什么。如果对所有的观察路径使用单个上下文,则需要首先根据通知上下文进行测试,找到匹配项,然后使用键路径字符串进行对比来确认具体更改了什么内容。如果为每个键路径提供了唯一的上下文,比如此处,一系列简单的指针比较就可以表明通知是否是此观察者的,及哪个键路径发生了改变。
在任何情况下,当观察者无法识别上下文(或在简单情况下,任何键路径)时应该始终调用父类的 observeValueForKeyPath:ofObject:change:context:的实现,因为这意味着父类注册了通知。
注意: 如果通知传播到类层次的顶部,NSObject将抛出NSInternalInconsistencyException异常,因为这是编程错误:一个子类无法使用为其注册的通知。
移除观察者
通过向被观察对象发送removeObserver:forKeyPath:context:消息并指定正在观察的对象、键路径和上下文可以删除键值观察者。
在收到removeObserver:forKeyPath:context:消息后,正在观察的对象将不在接收指定键路径和对象的任何observeValueForKeyPath:ofObject:change:context: 消息。
当删除观察者时,需要记住一下几点:
- 如果要求被移除的对象并没有注册为观察者将导致NSRangeException。可以对removeObserver:forKeyPath:context: 与 addObserver:forKeyPath:options:context: 相对应的进行一次调用,或者如果在应用中不可行的话,将removeObserver:forKeyPath:context:调用放在try/catch块中以处理潜在的异常。
- 当观察者释放时不会自动移除自己。被观察对象忽略观察者的状态,仍会发送通知。更改通知,像其他消息一样,向一个已经释放对象发送时将触发内存访问异常。因此需要确保观察者在从内存中消失时移除自己。
- 协议没有提供确认一个对象是观察者还是被观察的方法。故只能通过构造自己的代码来避免释放相关的错误。一个典型模式是在观察者初始化期间(如init或viewDidLoad)注册为观察者,在释放过程中(通常在dealloc)注销观察者,以确保正确配对且顺序的添加和删除消息,这样就确保在观察者从内存中释放前已注销。
KVO规范
为了特定的属性符合KVO规范,一个类需要确保如下:
- 该类必须符合属性的KVC规范,如确保KVC规范中所指定的那样。 KVO与KVC支持同样的数据类型,包括Objective-C对象、标量、结构体列表中支持的那些。
- 该类为属性发出KVO变更通知。依赖键已正确注册(见于注册依赖键)。有两种确保发出更改通知的技术。NSObject提供自动支持,且默认情况下可用于符合KVC规格的类的所有属性。通常情况下,如果遵守标准的Coaoca编码与命名规范,则可以使用自动变更通知而不需要写任何额外的代码。
- 手动变更通知需要额外的编码,提供了对当通知发送时的额外控制。可以实现automaticallyNotifiesObserversForKey:类方法来控制子类属性的自动通知。
自动变更通知
NSObject提供了自动键值变更通知的基本实现。自动键值更改通知将使用KVC合规的访问器和KVC方法来进行的更改通知给观察者。集合代理对象也支持自动通知,如mutableArrayValueForKey:。
手动变更通知
某些情况下,可能想控制通知过程,比如最大程度的减少因应用程序特定原因触发的不必要的通知或将多个更改组合为一个通知。手动更改通知在此场景提供意义。
手动和自动通知并不互斥。除了以发布的自动通知以外,还可以手动发布通知。通常,可能想完全控制特定属性的通知。在这种情况下,重写NSObjectautomaticallyNotifiesObserversForKey:的实现。对于要排除自动通知的属性,其子类automaticallyNotifiesObserversForKey:实现应该返回NO。子类无法识别任何键时应该调用父类的实现。
要实现手动观察者通知,需要在改变值前调用willChangeValueForKey:,改变之后调用didChangeValueForKey: 。
可以通过首先检查值是否已变更来最大程度地减少发送不必要的通知。
在有序的一对多关系案例中,不仅要指定已更改的键,还要指定更改的类型和涉及对象的索引。更改的类型是键为NSKeyValueChange值指定为NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement。受影响对象的索引作为NSIndexSet对象来传递。
注册依赖键
在很多情况下一个属性的值取决于另外一个对象中的一个或多个属性的值。如果一个属性的值变化了,则派生属性的值也应该标记为变更。如何确保为这些依赖属性发送KVO通知取决于关系的基数。
对一的关系
要为一对一关系自动触发通知需要重写keyPathsForValuesAffectingValueForKey:或按照其定义的用于注册依赖键的模式实现合适的方法。
通常重写应该调用super方法并返回一个包括会影响结果的所有成员的集合(以免干扰父类中此方法的重写)。
还可以通过实现遵守命名约定为keyPathsForValuesAffecting<Key>的类方法来达到同样的结果,其中<Key>是依赖于值的属性名称(首字母大写)。
当使用类别将计算属性添加到现有类中时,不能重写keyPathsForValuesAffectingValueForKey:方法,因为不应该充血类别中的方法。在这种情况下,实现匹配的keyPathsForValuesAffecting<Key>类方法并利用好此机制。
注意: 不能通过实现keyPathsForValuesAffectingValueForKey:来建立一对多的依赖关系。相反,必须观察对多集合中每个对象的适当属性,并通过更新相关键来响应其值的变化。
对多的关系
keyPathsForValuesAffectingValueForKey: 方法不支持对多关系的键路径。
两种情况下都有可能的解决方案:
-
可以使用KVO将父项注册为所有子项的相关属性的观察者。当子对象被从关系中加入或删除时必须将父项作为观察者在关系中也添加和删除。在observeValueForKeyPath:ofObject:change:context:方法中,可以更新依赖值以响应变更。
-
如果使用的是Core Data,则可以在应用的通知中心将父项注册为托管对象上下文的观察者。父项应以类似于KVOd的方式响应子项发布的相关变更通知。
KVO实现细节
自动KVO的实现是使用称为isa-swizzling的技术。
顾名思义,isa指针指向维护派发表的对象的类。该派发表实质上包含了指向该类实现的方法及其他数据。
当为对象的属性注册观察者时将修改对象的isa指针,其执行一个中间类而不是真正的类。结果导致isa指针不一定反映实例的实际类。
永远不应该依赖isa指针来确定类成员身份。相反,应该使用class方法来确定对象实例的类。