OC观察者模式之KVO的使用与思考
EAWorld引言:
无论用哪种语言进行软件开发,我们都会接触到设计模式,个人认为设计模式存在的意义在于:在某些需求下,采用适合的设计模式,使代码结构合理,从而提高代码的可读性、可扩展性、可移植性,此文将要讨论的是OC开发中的一种常用模式之一:观察者模式之KVO。
KVO俗称键值观察(key-value observe),键值观察是当被观察的对象属性发生改变时,会通知到观察对象的一种机制。
目录:
1、KVO的作用
2、KVO的使用方法
3、KVO的实现原理
4、KVO与KVC、代理、通知的区别
5、KVO实现过程中的注意事项
无论用哪种语言进行软件开发,我们都会接触到设计模式,个人认为设计模式存在的意义在于:在某些需求下,采用适合的设计模式,使代码结构合理,从而提高代码的可读性、可扩展性、可移植性,此文将要讨论的是iOS开发中的一种常用模式之一:观察者模式之KVO。我们先看下官方文档给的KVO介绍:
翻译过来就是:KVO是运用isa混写技术实现自动观察键值的。isa指针是指向对象的类,本质上是指向类中的方法实现。当一个对象注册观察者时,这个对象的isa指针被修改指向一个中间类。永远不要用isa来判断一个类的继承关系,而是应该用class方法来判断类的实例。
KVO俗称键值观察(key-value observe),键值观察是当被观察的对象属性发生改变时,会通知到观察对象的一种机制。
1.KVO的作用
1、监听带有状态的基础控件,如开关、按钮等;
2、监听字符串的改变,当监听的字符串改变时,来做一些自定义的操作;
3、当数据模型的数据发生改变时,视图组件能动态的更新,及时显示数据模型更新后的数据,比如tableview中数据发生变化进行刷新列表操作,监听 scrollView的contentOffset属性监听页面的滑动.
2.KVO的使用方法
KVO的使用可分为自动监听和手动监听。
1.自动监听
1.1自动监听操作步骤:
(1)添加观察者
(2)在观察者中添加观察键值方法
(3)在dealloc中移除监听
1.2示例代码:
创建两个类ModelA和ModelB,两个类中都添加属性“des”,在控制器中,将B添加为A的观察者。代码如下:
ModelA中代码:
ModelB中代码:
控制器中代码:
控制器中添加观察者的方法调用的是如下的类方法:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
各个参数说明:
@param observer 被监听的对象
@param keyPath 被监听对象的属性名,不可为空,为空崩溃
@param options 有4种
(1)NSKeyValueObservingOptionNew 把更改之前的值提供给处理方法
(2)NSKeyValueObservingOptionOld 把更改之后的值提供给处理方法
(3)NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
(4)NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后
@param context 上下文
上述示例代码的运行结果如下所示:
2.手动监听
意思就是说:当某些需要控制监听过程的场景下,就需要手动监听,比如:为了尽量减少不必要的触发通知操作,或者当多个更改同时具备的时候才调用属性改变的监听方法。
实现手动监听的要点主要包括这几部分:
a.重写
(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
b.在set方法中在赋值的前后分别调用
willChangeValueForKey和didChangeValueForKey
2.1实现部分属性的手动监听
在animal.h中添加两个属性age和name,在animal.m中关闭age的自动监听功能,其它属性依然可以自动监听,在控制其中实现添加按钮点击按钮的时候改变age的值,并触发监听方法,代码如下:
animal类:
要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对特定的 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理[1,2,3]。
控制器:
当不点击按钮的时候,打印结果只打印了name属性的值:
当点击按钮之后,会手动触发监听,打印结果如下:
2.2所有属性都手动监听(禁止自动监听)
如果需要禁用该类KVO的话直接automaticallyNotifiesObserversForKey返回NO。
将animal.m中的类方法修改之后:
运行之后不点击按钮的话,age和name属性都不会自动调用监听方法:
点击了按钮之后,只有实现了手动监听的age属性调用了监听方法:
3.KVO的实现原理
当某一个类的实例第一次使用KVO的时候,系统就会在运行期间动态的创建该类的一个派生类,该类的命名规则一般是以NSKVONotifying为前缀,以原本的类名为后缀。并且将原型的对象的isa指针指向该派生类。同时在派生类中重载了使用KVO的属性的setter方法,在重载的setter方法中实现真正的通知机制,正如前面我们手动实现KVO一样。这么做是基于设置属性会调用setter方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的[4,5]。
4.KVO与KVC、代理、通知的区别
1.与KVC的不同?
KVC,即是指 NSKeyValueCoding,一个非正式的 Protocol,提供一种机制来间接访问对象的属性,而不是通过调用Setter、Getter方法等 显式的存取方式去访问。KVO 就是基于 KVC 实现的关键技术之一。
KVO,即Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知。
2.与delegate的不同?
和delegate一样,KVO和NSNotification的作用都是类与类之间的通信。但是与delegate不同的是:这两个都是负责发送接收通知,剩下的事情由系统处理,所以不用返回值;而delegate 则需要通信的对象通过变量(代理)联系;delegate只是一对一,而这两个可以一对多。delegate是非常严格的语法,需要定义很多代码。
3.和notification的区别?
notification比KVO多了发送通知的一步。两者都是一对多,但是对象之间直接的交互,notification明显得多,需要notificationCenter来做为中间交互。而KVO如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句“交由系统通知”,具体的可参照以上实现过程的剖析。notification的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便[6,7]。
5.KVO实现过程中的注意事项
iOS 10以下会有这些情况,iOS11不会出现这些情况,但是为了代码的严谨性,以及以防出现无法预知的错误,还是避开这些比较好。
1、添加观察者次数与remove次数不匹配导致程序崩溃
连续对同一属性添加观察者是可以的,但是也要保证在移除观察者的时候也要移除对应次,不然可能会引发崩溃(iOS11以上不会崩溃)。
当对同一个keypath进行两次removeObserver时会导致程序crash,这种情况常常出现在父类有一个kvo,父类在dealloc中remove了一次,子类又remove了一次的情况下。不要以为这种情况很少出现!当你封装framework开源给别人用或者多人协作开发时是有可能出现的,而且这种crash很难发现。不知道你发现没,目前的代码中context字段都是nil,那能否利用该字段来标识出到底kvo是superClass注册的,还是self注册的?我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash[8]。
2、移除不存在的观察者(iOS11以上不会崩溃)
当某个对象并没有添加观察者时,却执行了移除观察者的操作,也会导致程序崩溃,此处不附相关代码。
3、被观察者销毁时还存在观察者(iOS11以上不会崩溃)
这种情况常出现在复杂逻辑下,观察者先于被观察者销毁[9]
4、KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange… 会触发 KVO 通知。
所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO[10]。