本文首发于个人博客
前言
什么是观察者模式
观察者模式 属于行为型模式。
观察者模式(有时又被称为模型(Model)-视图(View)模式、源-收听者(Listener)模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。
模式结构
角色
- 抽象主题(Subject):
它把所有观察者对象的引用保存到一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。 - 具体主题(Concrete Subject):
将有关状态存入具体观察者对象;在具体主题内部状态改变时,给所有登记过的观察者发出通知。 - 抽象观察者(Observer):
为所有的具体观察者定义一个接口,在得到主题通知时更新自己。 - 具体观察者(Concrete Observer):
实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题状态协调
使用场景:
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象需要被改变。
- 当一个对象必须通知其他对象,而它又不能假定其他对象是谁。换言之,不希望这些对象是紧密耦合的
优缺点
- 观察者模式的主要的作用就是对对象解耦,将观察者和被观察者完全隔离。
观察者模式的优点
- 观察者模式解除了主题和具体观察者的耦合,让耦合的双方都依赖于抽象,而不是依赖具体。
观察者模式的缺点
- 在应用观察者模式时需要考虑一下开发小路问题,程序中包括一个被观察者和多个被观察者,开发和调试比较复杂,而且Java中的消息的通知默认是顺序执行的,一个观察者的卡顿会影响整体的执行效率。在这种情况下,一般考虑采用异步的方式。
iOS中的观察者模式
一般两种:KVO和通知。通知比较简单,这里只说一下KVO
- KVO全称KeyValueObserving,俗称键值监听,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
- KVC和KVO都属于键值编程而且底层实现机制都是isa-swizzing。
- KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
- KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
实现原理
- KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。
- 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。当修改 instance 对象的属性时,会调用 Foundation框架的 _NSSetXXXValueAndNotify 函数 ,该函数里面会先调用 willChangeValueForKey: 然后调用父类原来的 setter 方法修改值,最后是 didChangeValueForKey:。didChangeValueForKey 内部会触发监听器(Oberser)的监听方法observeValueForKeyPath:ofObject:change:context:
- 并且将class方法重写,返回原类的Class。
KVO的使用
使用方法
- 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
- 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
- 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
例如,我们定义一个 YZPerson 类 继承自 NSObject ,里面有name 和 age 两个属性
1 | @interface YZPerson : NSObject |
然后在ViewController中,写如下代码
1 | - (void)viewDidLoad { |
执行之后结果为
1 | KVOdemo[11482:141804] 监听到<YZPerson: 0x6000004e8400>的name属性值改变了 - { |
注意点
需要注意的是,上面代码中我们已经移除了监听,如果再次移除的话,就会crash
例如
1 |
|
移除多次会报错
1 | KVOdemo[9261:2171323] *** Terminating app due to uncaught exception 'NSRangeException', |
如果忘记移除的话,有可能下次收到这个属性的变化的时候,会carsh
所以,我们要保证add和remove是成对出现的
资料
更多关于KVO的内容,包括KVO的本质,KVO内部的流程,手动调用KVO等,可以参考之前的一篇文章关于KVO看这篇就够了