设计模式-观察者模式KVO

1.什么是观察者模式?
2.为什么要用观察者模式?它的优缺点是什么?
![Uploading 屏幕快照 2016-12-20 下午3.23.56_660666.png . . .]
3.观察者模式解决了什么问题?你的项目中哪里用到了观察者模式?
4.观察者模式怎么用?有几种方式?
5.怎么创建观察者模式?
7.写观察者模式的时候需要注意什么问题?
8.iOS源代码中哪里用到了观察者模式?举例说明。
9.实现原理,

根据上面的套路,我们一一回答问题。
1.什么是观察者模式

KVO 是 Objective-C 对观察者设计模式的一种实现。【另外一种是:通知机制(notification)
KVO提供一种机制,指定一个被观察对象(例如A类),当对象某个属性(例如A中的字符串name)发生更改时,对象会获得通知,并作出相应处理;【且不需要给被观察的对象添加任何额外代码,就能使用KVO机制】
在MVC设计架构下的项目,KVO机制很适合实现mode模型和view视图之间的通讯。
例如:代码中,在模型类A创建属性数据,在控制器中创建观察者,一旦属性数据发生改变就收到观察者收到通知,通过KVO再在控制器使用回调方法处理实现视图B的更新;(本文中的应用就是这样的例子.)

简单的说就是,当某对象改变时,自动通知所有相关的状态进行更新。

2.为什么要用观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

观察者模式的优点:
1、 Subject和Observer之间是松偶合的,分别可以各自独立改变。
2、Subject在发送广播通知的时候,无须指定具体的Observer,Observer可以自己决定是否要订阅Subject的通知。
3、高内聚、低偶合。

3.观察者模式解决了什么问题?你的项目中哪里用到了观察者模式?
有时候我们需要监听某个类的属性值的变化从而做出相应的改变,这个时候使用KVO/KVC设计模式。
比如,在项目中,我需要监听model中的某个属性值的变换,当变化时,需要更新UI显示。

//增加frame的监听 
1)用于判断当前手势滑动的frame moveTableView:didChangeFromFrame:toFrame
2)增加搜索结果contentview 根据手势切换导航显示模式
    [self addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];

4.观察者模式怎么用?有几种方式?
在iOS中观察者模式的实现有四种方法:NSNotification、KVO、Protocol以及Code Block代码块。

要点:
Notification是一对多的,而delegate回调是一对一的。

Notification - NotificationCenter机制使用了操作系统的对象间通讯功能,而delegate是直接的函数调用。Notification跨度大,而delegate效率可能比较高。

相较于前两者KVO才是一种真正的观察者模式,它允许你将一个处理函数绑定到某个类的属性,属性发生改变是就会自动触发,不像其他两种需要你手动的发通知。KVO是一种非常灵活的观察机制,广泛应用于界面设计。

Code Block其实就相当于C的函数指针,可以用来做各种回调。我觉得其应当具备最高的效率。使用Code Block要注意的地方就是使用外部变量。在block里直接引用外部变量的话会在block定义的时候复制外部变量的一个拷贝,也就是说得到的是block定义时的值,在block内修改这个值也不会传给外部。要得到实时的数据,或者将数据传出的话需要在相关变量前面加__block即可。

NSNotification

设计模式-观察者模式KVO_第1张图片
屏幕快照 2016-12-20 下午3.22.55.png

发送通知:

设计模式-观察者模式KVO_第2张图片
屏幕快照 2016-12-20 下午3.25.41.png

实现通知的方法


设计模式-观察者模式KVO_第3张图片
屏幕快照 2016-12-20 下午3.23.56.png

KVO

//注册通知
 [_tableView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
//通知回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"frame"]) {
        //处理逻辑
    }
}
//移除通知
- (void)dealloc
{
    [_tableView removeObserver:self forKeyPath:@"frame"];
}

protocol/delegate

需要注意的问题:
1)A对象要通知B对象,B对象必须实现监听的方法,否则一旦有消息发送就会导致崩溃.

  1. A对象不想通知B对象了,需要从B对象身上移除掉通知.

但是要注意:添加的观察者的次数要和移除观察者的次数相等,少移除一个或者多移除一个都会造成程序崩溃:
3)严重依赖于string
KVO严重依赖string,换句话说,KVO中的keyPath必须是NSString这个事实使得编译器没办法在编译阶段将错误的keyPath给找出来;譬如很容易将「contentSize」写成「content size」;
4)

KVO的实现
KVO的实现也依赖于Objective-C的Runtime。
简单概述下KVO的实现:
当你观察一个对象(称该对象为「被观察对象」)时,一个新的类会动态被创建。这个类继承自「被观察对象」所对应类的,并重写该被观察属性的setter方法;针对setter方法的重写无非是在赋值语句前后加上相应的通知;最后,把「被观察对象」的isa指针(isa指针告诉Runtime系统这个对象的类是什么)指向这个新创建的中间类,对象就神奇变成了新创建类的实例。
根据文档的描述,虽然被观察对象的isa指针被修改了,但是调用其class方法得到的类信息仍然是它之前所继承类的类信息,而不是这个新创建类的类信息。
补充:下面对isa指针和类方法class作以更多的说明。
isa指针和类方法class的返回值都是Class类型,如下:
@interfaceNSObject {
ClassisaOBJC_ISA_AVAILABILITY;
}

  • (Class)class;
    根据我的理解,一般情况下,isa指针和class方法返回值都是一样的;但KVO底层实现时,动态创建的类只是重写了被观察属性的setter方法,并未重写类方法class,因此向被观察者发送class消息实际上仍然调用的是被观察者原先类的类方法+ (Class)class,得到的类型信息当然是原先类的类信息,根据我的猜测,isKindOfClass:和isMemberOfClass:与class方法紧密相关。
    国外的大神Mike Ash早在2009年就做了关于KVO的实现细节的探究,更多详细参考这里。

下面来对这两个参数进行详细介绍。
options
options可选值是一个NSKeyValueObservingOptions枚举值,到目前为止,一共包括四个值,在介绍这四个值各自表示的意思之前,先得有一个概念,即KVO响应方法有一个NSDictionary类型参数change(下面『响应』中可以看到),这个字典中会有一个与被监听属性相关的值,譬如被改变之前的值、新值等,NSDictionary中有啥值由『订阅』时的options值决定,options可取值如下:
NSKeyValueObservingOptionNew: 指示change字典中包含新属性值;
NSKeyValueObservingOptionOld: 指示change字典中包含旧属性值;
NSKeyValueObservingOptionInitial: 相对复杂一些,NSKeyValueObserving.h文件中有详细说明,此处略过;
NSKeyValueObservingOptionPrior: 相对复杂一些,NSKeyValueObserving.h文件中有详细说明,此处略过;
现在细想,options这个参数也忒复杂了,难怪大神们觉得这个API丑陋(不过我等小民之前从未想过这个问题,=_=,没办法,Apple是个大帝国,我只是其中一个跪舔的小屁民)。
不过更糟心的是下面的context参数。

context
options信息量稍大,但其实蛮好理解的,然而对于context,在写这篇博客之前,一直不知道context参数有啥用(也没在意)。
context作用大了去了,在上文『KVO的槽点』提到一个槽点『多次相同的removeObserver会导致crash』。导致『多次调用相同的removeObserver』一个很重要的原因是我们经常在addObserver时为context参数赋值NULL,关于如何使用context参数,下面的『响应』中会提到。

响应
iOS的UI交互(譬如UIButton的一次点击)有一个非常不错的消息转发机制 — Target-Action模型,简单来说,为指定的event指定target和action处理方法。
UIButtonbutton = [UIButtonnew];
[button addTarget:selfaction:@selector(buttonDidClicked:) forControlEvents:UIControlEventTouchUpInside];
这种target-action模型逻辑非常清晰。作为对比,KVO的响应处理就非常糟糕了,所有的响应都对应是同一个方法- (void)observeValueForKeyPath:ofObject:change:context:,其原型如下:
-(void)observeValueForKeyPath:(NSString
)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void *)context;
除了NSDictionary类型参数change之外,其余几个参数都能在–addObserver:forKeyPath:options:context:找到对应。

下面将针对「严重依赖于string」和「多次相同的removeObserver会导致crash」这两个槽点对keyPath和context参数进行阐述。
keyPath
keyPath的类型是NSString,这导致了我们使用了错误的keyPath而不自知,譬如将@”contentSize”错误写成@”contentsize”,一个更好的方法是不直接使用@”xxxoo”,而是积极使用NSStringFromSelector(SEL aSelector)方法,即改@"contentSize"为NSStringFromSelector(@selector(contentSize))。

context
对于context,上文已经提到一种场景:假如父类(设为ClassA)和子类(设为ClassB)都监听了同一个对象肿么办?是ClassB处理呢还是交给父类ClassA的observeValueForKeyPath:ofObject:change:context:处理呢?更复杂一点,如果子类的子类(设为ClassC)也监听了同一个对象,当ClassB接收到ClassC的[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];消息时又该如何处理呢?

用context参数判断!

在addObserver时为context参数设置一个独一无二的值即可,在responding处理时对这个context值进行检验。如此就解决了问题,但这需要靠用户(各个层级类的程序员用户)自觉遵守。

你可能感兴趣的:(设计模式-观察者模式KVO)