KVO/KVC是观察者模式在Objective-C中的实现,以非正式协议(Category)的形式被定义在NSObject中。从协议的角度看,是定义了一套让开发者遵守的规范和使用的方法。
在Cocoa的MVC框架中,KVO架起ViewController和Model沟通的桥梁,例如:
代码中,在模型类A创建属性数据,在控制器中创建观察者,一旦属性数据发生改变就收到观察者收到通知,通过KVO再在控制器使用回调方法处理实现视图B的更新。
实现原理
当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPath的setter方法,setter方法随后负责通知观察对象属性的改变状况。Apple使用了:
isa 混写(isa-swizzling)isa:is a kind of ; swizzling:混合,搅合;
来实现 KVO,当观察对象A时,KVO机制动态创建一个新的名为:
NSKVONotifying_A 的继承自对象A的子类,
且KVO为NSKVONotifying_A
重写了观察属性的setter方法,setter方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。
NSKVONotifying_A类剖析:
在观察到对象成员有变化时,被观察对象的isa指针从指向原来的A类,被KVO机制修改为指向:
系统新创建的子类 NSKVONotifying_A类,
通过NSKVONotifying_A
的setter方法来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统隐瞒了对KVO的底层实现过程,让我们误以为还是原来的类。
但是此时如果我们创建一个新的名为NSKVONotifying_A
的类,就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候:
动态的创建了名为NSKVONotifying_A的中间类,并指向这个中间类。
因此由于类名的冲突,就会引起程序的崩溃。
isa指针的作用:
每个对象都有isa指针,指向该对象的类,它告诉Runtime系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么:
这个被观察的对象就神奇地变成新子类的对象或者说实例了。
因此在该对象上对sette 的调用就会调用已重写的 setter,从而激活键值通知机制。
子类setter方法剖析:
KVO的键值观察通知依赖于NSObject 的两个方法:
willChangeValueForKey:
didChangevlueForKey:
被观察属性发生改变之前,
willChangeValueForKey:
被调用,通知系统该 keyPath的属性值即将变更;当改变发生后,
didChangeValueForKey:
被调用,通知系统该keyPath的属性值已经变更;在上面的方法执行之后:
observeValueForKey:ofObject:change:context:
也会被调用。且重写观察属性的setter方法是在运行时而不是编译时实现的。
KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
|
|
KVO的主要方法
KVO的主要方法有以下几个:
addObserver调用方法
|
|
addObserver
方法用于添加观察者,监听属性的变化,它的几个参数作用如下:
- object:是被观察者的实例;
- observer:是观察者的实例,这个对象要实现下面的
observeValue
方法; - forKeyPath:是观察者实例要监听的键,如UIView的frame、center等;
- options:有4个值,5种情况,具体请看下文;
- context:它是一个C指针,会指向希望监听的属性。如:
&object->name
,同时它也可以带入一些参数,可以是任意类型,需要自己强转。
observeValue处理方法
|
|
observeValue
方法用于监听属性的变化,使重写此方法的类具有监听的能力,它的几个参数作用如下:
- keyPath:对应forKeyPath
- object:被观察的对象
- change:对应options
- context:对应context
removeObserver移除属性观察者
|
|
需要注意的是,如果注册observer时传入了context
,之后一定要在合适的机会解除,否则会引发资源泄露。
options参数
options参数是OptionSet
类型,它是一个结构体,有以下四个类型属性:
|
|
这四个值的含义如下:
new:
表明变化的字典应该提供新的属性值,接收方法中使用change参数传入变化后的新值,键为NSKeyValueChange.newKey;
old:
表明变化的字典应该包含旧的属性值,接收方法中使用change参数传入变化前的旧值;键为NSKeyValueChange.oldKey;
initial:
把初始化的值提供给处理方法,一旦注册,马上就会调用一次,如果配置了new,change参数内容会包含新值;它是如何使用的呢?比如我们在进行UI相关的KVO操作时候,通常会遇到这样的需求:
在添加通知后,立即发送一个改变通知告诉UI去更新界面。
这会让我们的界面有一个初始状态。我们可以指定Initial
选项,这样在我们添加完KVO监听后,属性改变的通知就会立即被执行一次:
prior:
接收方法会在变化前后分别调用一次,共两次,change参数内容根据new和old的配置确定。这个选项可以让我们在被监听的属性改变的时候得到两个通知,一个是在属性值改变之前,一个是属性值改变之后。
然后就可以在 observeValueForKeyPath
中的change
字典中以NSKeyValueChangeNotificationIsPriorKey
键来表示当前通知是不是在属性被修改之前发送的:
|
|
当然,如果你只是想得到修改前和修改后的值,那么也可以用change
字段中的NSKeyValueChangeOldKey
和 NSKeyValueChangeNewKey
来得到相应的值:
|
|
change参数
change参数是NSKeyValueChangeKey
类型,它是一个结构体:
|
|
它的几个参数作用如下:
kindKey:用于传递被监听属性的变化类型
123456public enum NSKeyValueChange : UInt {case settingcase insertioncase removalcase replacement}- setting:属性的值被重新设置;
- insertion & removal & replacement:表示更改的是集合属性,分别代表插入、删除、替换操作。
newKey:如果KindKey是setting,那么newKey对应的为新设置的值;
oldKey:如果KindKey是setting,那么oldkey对应的为上一个值;
indexesKey:如果kindkey是.insertion, .removal, .replacement,那么这个值对应的为NSIndexSet (包含相应操作的indexs);
priorKey:表明收到的是值改变之前的通知,对应着addobserver中option的.prior;
了解KVO的主要方法和参数后,我们就可以开始使用KVO进行编程了,下面我们开始进行:
KVO的使用
首先定义一个 Persson
类,当做被观察的对象:
|
|
再定义一个 PersonObserving
类并重写observeValue
方法,用于观察Person
:
|
|
观察者和被观察者准备就绪,即可进行测试:
|
|
KVO和keyPath
如果Person
类里面还有个再添加一个Job
的属性:
|
|
要观察和设置Person
的薪水,只要这么写就可以了。
KVO属性依赖
假如有个Person
类,类里有三个属性,fullName
、firstName
、lastName
。按照之前的知识,如果需要观察名字的变化,就要分别添加 fullName
、firstName
、lastName
三次观察,非常麻烦。
如果能够只观察 fullName
,并建立fullName
和 firstName
、lastName
的某种依赖关系,当发生变化时,也受到通知,那需要怎么做呢?KVC提供这种键之间的依赖方法,格式如下:
|
|
这方法使得Key之间能够建立依赖关系,为了便于说明,直接用属性依赖这个词代替Key之间的依赖。含义不同,结果一致,下面就使用这种方法解决 Key 之间的依赖关系,首先将Person
类设置为被观察者:
|
|
将ViewController
类设置为观察者:
|
|
通过输出结果,我们可以判断出:发现虽然观察的是fullName
,但是当修改firstName
的时候,观察者也会收到fullName
变化的通知,达到了我们的期望。
重写keyPathsForValuesAffectingValueForKey
方法的时候有一点需要注意,这个方法在Objective-C中的签名是这样:
(NSSet )keyPathsForValuesAffectingValueForKey:(NSString )key;
如果照着这个逻辑,我们很可能在 Swift 中将这个方法的声明写成这样:
override class func keyPathsForValuesAffectingValueForKey(key: NSString) -> NSSet;
如果这样写,编译器就会报错。因为Swift 中将Objective-C的一些基础类型都已经转变成了Swift的原生类型,所以我们这个方法签名要写成这样才可以通过编译:
override class func keyPathsForValuesAffectingValueForKey(key: String) -> Set。
KVO手动通知
KVO 在默认情况下,只要为某个属性添加了监听对象,在这个属性值改变的时候,就会自动的通知监听者。也有一些情况下,可能我们想手动的处理这些通知的发送, KVO 也是允许我们这样做的。我们可以通过重写
class func automaticallyNotifiesObservers(forKey key:String) -> Bool
这个方法告诉KVO,哪些属性是我们想手动处理的,比如我们的Person类中,想对firstName
进行处理,就在方法中对firstName
这个key返回false
:
dynamic关键字
注意下面我们使用了dynamic
标识。这个代表它支持Objective-C Runtime的动态分发机制,我们这里可以理解为 KVO需要使用Objective-C的Runtime机制来实现属性更改的监听。
Swift中的属性处于性能等方面的考虑默认是关闭动态分发的,所以我们这里面要显式的将属性用dynamic
关键字标识出来。
|
|
这样,我们在修改了fistName
属性值后,并不会触发KVO的默认通知行为,是我们自己来控制通知的发送,当然,这需要我们修改firstName
属性的实现来进行手工的通知发送,手动通知能让我们对KVO通知进行更细节的控制。但并不常用,大多数情况下使用KVO的自动通知机制就足够了。
KVO和线程
一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用-didChange...
会触发 KVO 通知。
所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把KVO和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO。
KVO是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO会保证下列两种情况的发生:
首先,如果我们调用一个支持KVO的setter方法,如下所示:
1self.name = "conqueror"KVO能保证所有
name
的观察者在setter
方法返回前被通知到。其次,如果某个键被观察的时候附上了
NSKeyValueObservingOptionPrior
这个选项,直到1override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)被调用之前,
name
的getter
方法都会返回同样的值。
理解KVO的实现
基本的流程就是编译器自动为被观察对象创造一个派生类,并将被观察对象的isa指向这个派生类。如果用户注册了对某此目标对象的某一个属性的观察,那么此派生类会重写这个方法,并在其中添加进行通知的代码。
Objective-C在发送消息的时候,会通过isa指针找到当前对象所属的类对象。而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。
由于编译器对派生类的方法进行了override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法。
总结
KVO是Cocoa提供的一个很强大的特性,但同时它也有很多坑需要我们注意,比如添加完监听后,要在不需要的时候删除掉监听,否则就会造成意外崩溃。对于有依赖关系的属性需要通过:
|
|
这个方法将依赖关系声明到实体类中。还有各个监听选项的作用不同组合也有不同的作用;
另外需要注意的是观察者观察的是属性,只有遵循KVO变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。
如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量,例如:仅调用person.name = "newName"
,这时是不会触发KVO机制,所以使用KVO机制的前提是遵循 KVO 的属性设置方式来变更属性值。
对比其他的回调方式,KVO机制的运用的实现,更多的由系统支持,相比Notification、Delegate等更简洁些,并且能够提供观察属性的最新值以及原始值;但是相应的在创建子类、重写方法等等方面的内存消耗是很巨大的。
由于KVO对被观察者的继承和重写是在运行时而不是编译时实现的,如果给定的实例没有观察者,那么KVO不会有任何开销,因为此时根本就没有KVO代码存在。但是即使没有观察者,Delegate和NSNotification还是得工作,这也是KVO此处零开销观察的优势。
所以对于两个类之间的通信,我们可以根据实际开发的环境采用不同的方法,使得开发的项目更加简洁实用。
KVC与KVO的区别
KVC(键值编码),即Key-Value Coding,一个非正式的Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用Setter、Getter方法等显式的存取方式去访问。
KVO(键值监听),即Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了Setter方法、或者使用了KVC赋值。
KVO与Notification的区别
Notification比KVO多了发送通知的一步,两者都是一对多,但是对象之间直接的交互,Notification需要NotificationCenter来做为中间交互。而KVO如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了。
Notification的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。
KVO与Delegate的区别
和Delegate一样,KVO和NSNotification的作用都是类与类之间的通信。
但是KVO和NSNotification与Delegate不同的是,KVO和NSNotification都是负责发送接收通知,剩下的事情由系统处理,所以不用返回值;
而Delegate则需要通信的对象通过变量(代理)联系;Delegate一般是一对一,而KVO和NSNotification可以一对多。
参考链接
KVC和KVO - 卢思豪
iOS–KVO的实现原理与具体应用 - 啊左
漫谈 KVC 与 KVO - SwiftCafe