设计模式之(九)观察者模式

本文首发于个人博客

前言

什么是观察者模式

观察者模式 属于行为型模式。

观察者模式(有时又被称为模型(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的使用

使用方法

  1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
  2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
  3. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。

例如,我们定义一个 YZPerson 类 继承自 NSObject ,里面有name 和 age 两个属性

1
2
3
4
@interface YZPerson : NSObject
@property (nonatomic ,assign) int age;
@property (nonatomic,strong) NSString *name;
@end

然后在ViewController中,写如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)viewDidLoad {
[super viewDidLoad];
//调用方法
[self setNameKVO];
}

-(void)setNameKVO{
self.person = [[YZPerson alloc] init];
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];

}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"ccc";

}

-(void)dealloc
{
// 移除监听
[self.person removeObserver:self forKeyPath:@"name"];
}

执行之后结果为

1
2
3
4
5
KVOdemo[11482:141804] 监听到<YZPerson: 0x6000004e8400>的name属性值改变了 - {
kind = 1;
new = ccc;
old = "<null>";
} - 1111- 1111

注意点

需要注意的是,上面代码中我们已经移除了监听,如果再次移除的话,就会crash

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

- (void)viewDidLoad {
[super viewDidLoad];
//调用方法
[self setNameKVO];
}
-(void)setNameKVO{
self.person = [[YZPerson alloc] init];
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
// 移除监听
[person removeObserver:self forKeyPath:@"name"];
// 再次移除
[person removeObserver:self forKeyPath:@"name"];

}

移除多次会报错

1
2
3
KVOdemo[9261:2171323] *** Terminating app due to uncaught exception 'NSRangeException', 
reason: 'Cannot remove an observer <ViewController 0x139e07220> for the key path "name"
from <YZPerson 0x281322f20> because it is not registered as an observer.'

如果忘记移除的话,有可能下次收到这个属性的变化的时候,会carsh

所以,我们要保证add和remove是成对出现的

资料

更多关于KVO的内容,包括KVO的本质,KVO内部的流程,手动调用KVO等,可以参考之前的一篇文章关于KVO看这篇就够了