深度理解Key-Value Observing 键值观察

前言

 

在上一阶段的开发过程中,我们大量使用了 KVO 机制,来确保页面信息的及时同步。也因此碰到了很多问题,促使我们去进一步学习 KVO 的相关机制,再到寻找更好的解决方案。鉴于 KVO 让人欲仙欲死的使用经历,在这里做一个简单分享。此分享的目的,更多的是在于点出 KVO 相关的技术点,供我们大家在学习和使用过程中做一个参考。

对于 KVO 的背后机制感兴趣的同学,可以直接看第三部分,KVC 和 isa-swizzling 。

对于 替代方案感兴趣的同学,请直接跳到末尾的第五部分,有列出了目前 github 上使用广泛的几个开源项目,它们让 KVO 变的更易用,总有一款适合你。如果各位有好的推荐,也请务必在评论里告诉我们,不胜感激。

对集合对象的观察,会在下次更新时做一个更具体的补充。

如果此文中有不当或错误的地方,也请各位批评指正,非常感谢~  没说的,报上工位号,请喝可乐~

一、什么是 KVO

键值观察是 Objective-C 语言的动态语言特性,在运行时通过 KVO,允许一个对象观察另一个对象的属性,当变化发生时,观察者会得到通知。

键值观察实际上是观察者模式在 Objective-C 中的一种运用,理解了观察者模式,也就理解了键值观察。

a)观察者和被观察对象完美分离

b)保持信息同步

二、通过一个示例了解 KVO 的基本用法

使用 KVO 的过程基本上分为三步:注册—通知—取消注册

下面来看一个示例。

某人养了一只宠物狗,这只宠物狗非常的聪明,会做加法题,而且很快。

我们在这里用 Person 来表示某人,Dog 来表示这只宠物狗,Person 有两个属性分别表示加数和被加数。

        @property (nonatomic, assgin) int numberOne;

        @property (nonatomic, assgin) int numberTwo;

1、Dog注册成为观察者,Person 为被观察对象,观察的属性为 numberOne、numberTwo

Dog:

        [person addObserver: self 

                 forKeyPath: @“numberOne”

                    options: NSKeyValueObservingOptionNew 

                    context: nil];     

        [person addObserver: self 

                 forKeyPath: @“numberTwo”

                    options: NSKeyValueObservingOptionNew 

                    context: nil];

2、Person 出题,Dog 收到通知后回答题目

Person 出题并发出通知:

        self.numberOne = 2;

        self.numberTwo = 3;

Dog 收到通知,回答题目:

        - (void) observeValueForKeyPath: (NSString *)keyPath 

                               ofObject: (id)object 

                                 change: (NSDictionary *)change 

                                context: (void *)context {

          // 根据 keyPath  判断是加数还是被加数发生变化,从 change 中获取新值,计算结果

        }

3、取消注册

Dog:

        [person removeObserver:self forKeyPath:@(numberOne)];

        [person removeObserver:self forKeyPath:@(numberTwo)];

三、KVO 原理详解

要理解 KVO 的原理,实际上也就是要搞清楚被观察对象在属性发生变化时,是如何做到通知观察者的。

这里面包含有两个点,一个是对属性的读取,一个是通知。

1、属性读取

说到对属性的读取,就不得不提 KVC,key-value coding,键值编码。实际上这也是 KVO 的基础。

KVC 提供了一种通过字符串标识符间接访问对象属性的机制。

1)支持这种机制的基本方法是:

        - (id) valueForKey:(NSString *)key;

        - (void) setValue:(id)value forKey:(NSString *)key;

例如访问 Person 对象的 numberOne 属性,可以通过以下方法实现:

        [person valueForKey:@“numberOne”];

        [person setValue:@(1) forKey:@“numberOne”];

对于实现了访问器方法的类来说,通过访问器方法(点语法)和通过 KVC 访问属性区别不大。但是对于没有实现访问器方法的类来说,点语法不可用,但是我们仍然可以通过 KVC 来访问属性。

下面来具体看下 KVC 访问属性时发生了什么。

KVC为了能设置和返回对象属性,会按照如下顺序进行尝试:

a)检查是否存在 - <key>,- is<Key> (只对布尔型有效),- get<Key> 的访问器方法,如果存在,则使用这些方法返回属性值。

检查是否存在 - set<Key> 的访问器方法,如果存在,则使用这些方法设置属性值。

b)如果上述方法不可用,则检查  - _<key>,- _is<Key> (只对布尔型有效),- _get<Key>,- _set<Key> 方法是否可用。

c)如果没有找到上述方法,会尝试直接访问实例变量,实例变量名可以是 <key> 或 _<key>

d)如果仍未找到,则调用 - valueForUndefinedKey: 和 - setValue:forUndefinedKey: 方法。这些方法的默认实现是抛出异常,我们可以根据需要进行重写。

由此我们也可以看出,当属性读取方法的定义符合命名规范的时候,KVC 能够定位到 键 key 对应的属性读取方法。

        // 属性访问器命名规范:     

        - (type) name;

        - (void) setName:(type)newName;

        

        // 特殊的:

        - (BOOL) isHidden;

        - (void) setHidden:(BOOL)newHidden;

2)除了基本方法之外,KVC 还提供了如下方法来支持通过键路径访问嵌套对象的属性

        - (id) valueForKeyPath:(NSString *)keyPath;

        - (void) setValue:(id)value forKeyPath:(NSString *)keyPath;

以及其它的一些方法,来支持对多关系的属性的读取。

* 对多关系的属性的读取,请参考 KVC 的相关文档

2、在属性读取方法里面,通知被观察者

这一步的实现,是基于 isa-swizzling (指针变化) 技术。

1)isa 是对象的一个特定指针,它指向对象的类, 该类中包含一张调度表,反映出选择器和最终实现之间的映射关系。当某个对象被第一次观察时,系统会在运行期动态创建一个派生类,isa 会指向这个新诞生的派生类。

例如我们之前的例子中,Person 对象的 isa 指针 在观察之前会指向 Person,在观察之后会指向 NSKVONotifying_Person

* 关于指针变换技术,大家可以参考 method swizzling http://www.cocoachina.com/applenews/devnews/2014/0225/7880.html

* 这个映射关系是可以更改的,涉及到 objective-c 的运行时技术,objc/runtime.h

* isa 指针的变化大家可以在代码中设置断点观察到

2)派生类会重写基类中任何被观察属性的 setter 方法, 真正的通知机制,正是在这个被重写的 setter 方法里面实现的。

例如:

        // 之前

     

        - (void) setNumberOne:(int)numberOne {

            _numberOne = numberOne;

        }

     

        

        //之后

        - (void) setNumberOne:(int)numberOne {

            [self willChangeValueForKey:@“numberOne”];

            _numberOne = numberOne;

            [self didChangeValueForKey:@“numberOne”];

        }

3、使用 KVO 通知观察者方法小结

1)使用 KVC 方法

如果有访问器方法,则运行时会在访问器方法中调用 will/didChangeValueForKey: 方法;

没用访问器方法,运行时会在 setValue:forKey 方法中调用 will/didChangeValueForKey: 方法。

2)使用访问器方法

运行时会重写访问器方法调用 will/didChangeValueForKey: 方法。因此,直接调用访问器方法改变属性值时,KVO也能监听到。

3)在赋值前后,手动调用 will/didChangeValueForKey: 方法。

四、实践

1、特点总结:

1)优点

  • 提供了一种简单方法让对象之间保持信息同步。例如模型对象和视图对象
  • 能够让我们观察某个对象的状态变化,即便该对象不是由我们创建的,也不能更改状态属性的实现方法
  • 观察对象可以了解该属性值新值以及旧值;如果观察的属性为对多的关系(例如数组),它也能够了解是哪个包含的对象发生了改变
  • 能够使用键路径 keypath 观察嵌套对象的属性变化
  • 彻底的抽象化,一个对象并不需要额外的代码来让自己变成可被观察对象
  • 多个 KVO 观察者可以观察同一对象的同一属性

2)缺点

  • 必须用字符串来指定要观察的属性,因此如果出错,在编译时是不会有检查和警告的
  • 重构对象属性之后,相关的 KVO 代码将不再起作用,由于不会有编译时自动检查,这部分代码甚至会引起崩溃
  • KVO 通知会触发一个特定的观察方法,观察必须要实现该方法,当观察者在观察多个属性时,在该方法中要写复杂的 if else 语句进行判断
  • 对象在销毁时要移除注册过的观察者

2、实际使用过程中,要特别注意的要点

1)在调用注册方法时传入适当的参数

        - addObserver:forKeyPath:options:context:

options:

功  能
NSKeyValueObservingOptionNew 作为变更信息的一部分发送新值
NSKeyValueObservingOptionOld 作为变更信息的一部分发送旧值
NSKeyValueObservingOptionInitial 在观察者注册时发送一个初始更新
NSKeyValueObservingOptionPrior 在变更前后分别发送变更,而不只在变更后发送一次

属性值的新值和旧值相同时,仍然能够触发 KVO,我们在注册时知道 new 和 old,能够让我们在通知方法中判断新旧值是否相同。

initial 可以确保在注册的同时,就触发一次 KVO 通知。

context:

由于我们无法指定通知方法,当在有通知发生时,如果子类和父类都实现了该方法,那么子类在处理通知时,无法通过 keyPath 和 object 来准确判断父类是否对该通知感兴趣,这个时候就需要子类父类在注册时根据需要传入不同的 context

* 关于 context 的最佳实践可以参考 http://stackoverflow.com/questions/12719864/best-practices-for-context-parameter-in-addobserver-kvo

2)手动触发 KVO

KVO 协议提供一个方法来关闭自动 KVO 通知:

    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key

返回值为 NO 时,我们无论是通过 KVC 方法,还是访问器方法,都不会触发 KVO,我们需要自己手动调用will/didChangeValueForKey: 方法

3)使用单个 key 观察多个属性的变化

通过重写以下 方法,可以仅注册单个键的观察多个属性的变化:

    + (NSSet *) keyPathsForValuesAffectingValueForKey:(NSString *)key

4)KVO 支持对集合对象(NSArray、NSSet、NSOrderedSet)的观察,来及时获得集合内元素发生的变化,例如集合元素的增加、删除等等,变化的类型和具体内容会包含在通知方法的 change 字典中。

例如我们用 array 作为 tableView 的数据源,使用 kvo 方式观察 array 的变化,来自动触发 tableView 的刷新。

值得注意的是,我们没有办法使用 key path 来观察集合内部某元素的属性变化,要做到这一点,我们需要在往集合内添加和删除元素时,为每个元素单独注册和取消注册 KVO。

5)在通知方法中要注意线程问题

通知方法在哪个线程中被调用,是由被观察对象在哪个线程中触发 kvo 决定的。

6)不正确的取消注册会导致程序崩溃

a)不能重复取消相同的注册

b)如果是类似 @“a.b.c”  键路径,在取消注册时,a b 对象应当是存在的 。

针对原则 a,大家可以自行思考可以用什么方法来避免重复的注册和取消注册;

针对原则 b,需要注意的是我们在取消注册时,键路径中的对象是不是已经被释放了。

基于这两个原则,对于某些 UI 对象,除了考虑在 dealloc 中要取消注册外,还要根据实际情况来判断具体在什么位置注册和取消注册。以下是我在使用过程中遇到的,需要思考是否有必要做 注册和取消注册 的一些方法。

        // 复用的 cell

        - (void) prepareForReuse

        

        // 非复用的 view

        - (void) willMoveToWindow:(UIWindow *)newWindow

        - (void) didMoveToWindow

               

        - (void) didMoveToSuperview

        - (void) willMoveToSuperview:(UIView *)newSuperview

        

        // view controller     

        - (void) willMoveToParentViewController:(UIViewController 

        - (void) didMoveToParentViewController:(UIViewController *)parent*)parent

        - (void) viewDidLoad

五、更易用的 KVO

有一些开源项目,对 KVO 进行了二次封装,让 KVO 变的更易用,更安全,下面列举一些使用较广泛的供大家参考。在项目页面上,已经有了详细的特点说明和使用方法。

1)https://github.com/facebook/KVOController  推荐使用

2)https://github.com/th-in-gs/THObserversAndBinders

3)https://github.com/mikeash/MAKVONotificationCenter

六、参考文档:

Key-Value Observing Programming Guide

Key-Value Coding Programming Guide

http://blog.csdn.net/wzzvictory/article/details/9674431

http://blog.csdn.net/kesalin/article/details/8194240

http://www.cnblogs.com/lwzz/archive/2013/04/25/3029679.html

https://www.mikeash.com/pyblog/key-value-observing-done-right.html

深度理解Key-Value Observing 键值观察

时间: 2024-10-20 10:38:16

深度理解Key-Value Observing 键值观察的相关文章

K-V-C 键值观察机制

在两个不同的控制器之间传值是iOS开发中常有的情况,应对这种情况呢,有多种的应对办法.kvc就是其中的一种,所以,我们就在此解释之.   key value observing  键值观察,给人一种高冷的感觉,其实,我们可以用一个通俗的例子来解释之.就拿美俄之间的间谍来举例子.美俄是两个各自独立的国家,但是为了各自的利益,彼此之间勾心斗角,不断的爆出间谍丑闻.打住!从政治的深渊回到技术层面O(∩_∩)O.美国想知道俄罗斯的最新的导弹技术,于是派间谍收集情报,(kvo的第一步:注册观察者-美国,监

xcode KVC:Key Value Coding 键值编码

赋值 // 能修改私有成员变量 - (void)setValue:(id)value forKey:(NSString *)key; - (void)setValue:(id)value forKeyPath:(NSString *)keyPath; - (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues; 取值 // 能取得私有成员变量的值 - (id)valueForKey:(NSString *)key; - (

[深入浅出Cocoa]详解键值观察(KVO)及其实现机理

一,前言 Objective-C 中的键(key)-值(value)观察(KVO)并不是什么新鲜事物,它来源于设计模式中的观察者模式,其基本思想就是: 一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象.这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的.观察者模式较完美地将目标对象与观察者对象解耦. 在 Objective-C 中有两种使用键值观察的方式:手动或自动,此外还支持注册依赖键(即一个键依赖于其他键,其他键的变化也会作用到该键).下面将一一

obj-c编程17:键值观察(KVO)

说完了前面一篇KVC,不能不说说它的应用KVO(Key-Value Observing)喽.KVO类似于ruby里的hook功能,就是当一个对象属性发生变化时,观察者可以跟踪变化,进而观察或是修正这个变化,这是通过回调观察者注册的回调函数来完成的.要使用键值观察,必须满足3个条件: 1 被观察对象必须对所观察属性使用符合KVC标准的存取器方法: 2 观察者必须实现接受通知的方法(回调方法):-observeValue:forKeyPath:ofObject:change:context:,该方法

KVO键值观察简述

KVO 键值观察,简单来说就是为一个key添加一个观察者,当key的值发生改变的时候会发送通知,在接到通知的时候会有回调方法被调用 #import "ViewController.h" @interface ViewController (){     NSMutableDictionary * myDict; } @end @implementation ViewController - (IBAction)dasdas:(id)sender {          //改变key的值

KVO(键-值观察)

// 1.键-值观察 // 2.它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知. // 3.符合KVC(Key-ValuedCoding)机制的对象才可以使用KVO // 4.实现过程 // ①注册,指定被观察者 // ②实现回调方法 // ③移除观察 - (void)viewDidLoad {     [super viewDidLoad];     // Do any additional setup after loading the view from its nib.

Rx 键值观察KVO的使用

键值观察KVO的使用 1,KVO 介绍 KVO(键值观察)是一种 Objective-C 的回调机制,全称为:key-value-observing. 该机制简单来说就是在某个对象注册监听者后,当被监听的对象发生改变时,对象会发送一个通知给监听者,以便监听者执行回调操作. 2,RxSwift 中的 KVO RxCocoa 提供了 2 个可观察序列 rx.observe 和 rx.observeWeakly,它们都是对 KVO 机制的封装,二者的区别如下. (1)性能比较 rx.observe 更

iOS 键值观察(KVO)简述及实例理解

KVO概述: KVO,即:Key-Value Observing,直译为:基于键值的观察者.  它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知. 简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了.KVO的优点: 当有属性改变,KVO会提供自动的消息通知.这样开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知. 这是KVO机制提供的最大的优点.因为这个方案已经被明确定义,获得框架级支持,可以方便地采用. 开发人员不需要添加任何代码,

键-值观察

若想成为一个键的观察者,可添加如下代码. [theAppDelegate addObserver:self forKeyPath:@"fido" options:NSKeyValueObservingOptionOld context:nil]; 上述方法定义在NSObject中,实际上类似于说,“无论何时fido改变了就给我发个消息”,options和context决定fido改变时将哪些额外的数据与消息一起发送出去.触发方法过程如下 -(void)observeValueForKe