一、什么是 KVO
首先让我们了解一下什么KVO,全称为Key-Value Observing,是iOS中的一种设计模式,用于检测对象的某些属性的实时变化情况并作出响应。键值观察Key-Value-Observer就是观察者模式。
观察者模式的定义:一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。
KVO和KVC没有什么关系,要说有关系的话也就是--KVO同KVC一样都依赖于Runtime的动态机制.
在WPF中有一种双向绑定机制,如果数据模型修改了之后会立即反映到UI视图上,类似的还有如今比较流行的基于MVVM设计模式的前端框架。其实在ObjC中原生就支持这种机制,它叫做Key Value Observing(简称KVO)。KVO其实是一种观察者模式,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身。在ObjC中要实现KVO则必须实现NSKeyValueObServing协议,不过幸运的是NSObject已经实现了该协议,因此几乎所有的ObjC对象都可以使用KVO。
二、怎么实现 KVO
- 注册
1 //keyPath就是要观察的属性值 2 //options给你观察键值变化的选择 3 //context方便传输你需要的数据 4 -(void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath 5 options:(NSKeyValueObservingOptions)options context:(void *)context;
- 实现方法
1 //change里存储了一些变化的数据,比如变化前的数据,变化后的数据;如果注册时context不为空,这里context就能接收到。 2 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 3 4 change:(NSDictionary *)change context:(void *)context
- 移除
1 //移除 2 - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
三、 KVO底层实现分析
系统实现KVO有以下几个步骤:
当类A的对象第一次被观察的时候,系统会在运行期动态创建类A的派生类。我们称为B。
在派生类B中重写类A的setter方法,B类在被重写的setter方法中实现通知机制。
类B重写会 class方法,将自己伪装成类A。类B还会重写dealloc方法释放资源。
系统将所有指向类A对象的isa指针指向类B的对象。
通俗一点的解释是:当注册观察者的时候做的哪些事情:
1.动态的创建一个叫NSKVONotifying_Person的子类
2.更改之前类的 isa指针为子类
3.传入一堆参数 1.监听者(将来调用observeValueForKeyPath) 2.keypath(决定了重写哪个set方法) 3.枚举(决定传哪些给你) 4.携带参数
4.根据keypath 重写子类的set方法
1 //其实在子类的set方法中是实现了下面三步 2 [super setWeight:weight]; 3 4 //这两个方法会调用监听者的监听者方法 5 [self willChangeValueForKey:@"weight"]; 6 [self didChangeValueForKey:@"weight"];
5.在子类的set方法中 根据枚举 保存所有的属性值 然后调用父类的set方法 然后调用监听者的observeValueForKeyPath... 把对应的值传出去通知监听者发生了事情。所以不能依靠isa指针来确定对象是否是一个类的成员。应该使用class方法来确定对象实例的类。
四、KVO使用陷阱介绍:
首先,看一下KVO的使用场景,假设我们的目标是在一个UITableViewController内对tableview的contentOffset进行实时监测,很容易地使用KVO来实现为[使用场景]。
在初始化方法中加入:
1 [_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
在dealloc中移除KVO监听:
1 [_tableView removeObserver:self forKeyPath:@"contentOffset" context:nil];
添加默认的响应回调方法:
1 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 3 change:(NSDictionary *)change context:(void *)context 5 { 7 [self doSomethingWhenContentOffsetChanges]; 9 }
通常的写法已经完成,但是当你在controller中添加多个KVO时,所有的回调都是走同上述函数,那就必须对触发回调函数的来源进行判断。判断如下:
1 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 2 change:(NSDictionary *)change context:(void *)context 3 { 4 if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) { 5 [self doSomethingWhenContentOffsetChanges]; 6 } 7 }
接着还有其他的陷阱,如 我们假设当前类(在例子中为UITableViewController)还有父类,并且父类也有自己绑定了一些其他KVO. 我们看到,上述回调函数体中只有一个判断,如果这个if不成立,这次KVO事件的触发就会到此中断了。但事实上,若当前类无法捕捉到这个KVO,那很有可能是在他的superClass,或者super-superClass...中,上述处理截断了这个链。合理的处理方式应该是这样的:
1 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 2 change:(NSDictionary *)change context:(void *)context 3 { 4 if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) { 5 [self doSomethingWhenContentOffsetChanges]; 6 } else { 7 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 8 } 9 }
还有潜在的问题有可能出现在dealloc中对KVO的注销上。KVO的一种缺陷(其实不能称为缺陷,应该称为特性)是,当对同一个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。