KVO中你所不知道的"坑"
一、什么是 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。
KVO中你所不知道的"坑"的更多相关文章
- JavaScript中你所不知道的Object(二)--Function篇
上一篇(JavaScript中你所不知道的Object(一))说到,Object对象有大量的内部属性,而其中多数和外部属性的操作有关.最后留了个悬念,就是Boolean.Date.Number.Str ...
- JavaScript中你所不知道的Object(一)
Object实在是JavaScript中很基础的东西了,在工作中,它只有那么贫瘠的几个用法,让人感觉不过尔尔,但是我们真的了解它吗? 1. 当我们习惯用 var a = { name: 'tarol' ...
- Go基础之--位操作中你所不知道的用法
之前一直忽略的就是所有语言中关于位操作,觉得用处并不多,可能用到也非常简单的用法,但是其实一直忽略的是它们的用处还是非常大的,下面先回顾一下位操作符的基础 位操作符 与操作:&1 & ...
- 前端开发 CSS中你所不知道的伪类与伪元素的区别--摘抄
做过前端开发的人都熟悉伪类与伪元素,而真正能够彻底了解这二者的区别的人并不多.伪类与伪元素确实很容易混淆. 伪元素主要是用来创建一些不存在原有dom结构树种的元素,例如:用::before和::aft ...
- Visual Studio中你所不知道的智能感知
在Visual Studio中的智能感知,相信大家都用过.summary,param,returns这几个相信很多人都用过的吧.那么field,value等等这些呢. 首先在Visual Studio ...
- Android中Context详解 ---- 你所不知道的Context
转自:http://blog.csdn.net/qinjuning/article/details/7310620Android中Context详解 ---- 你所不知道的Context 大家好, ...
- 你所不知道的html5与html中的那些事第三篇
文章简介: 关于html5相信大家早已经耳熟能详,但是他真正的意义在具体的开发中会有什么作用呢?相对于html,他又有怎样的新的定义与新理念在里面呢?为什么一些专家认为html5完全完成后,所有的工作 ...
- Android中Context详解 ---- 你所不知道的Context(转)
Android中Context详解 ---- 你所不知道的Context(转) 本文出处 :http://b ...
- 你所不知道的html5与html中的那些事(三)
文章简介: 关于html5相信大家早已经耳熟能详,但是他真正的意义在具体的开发中会有什么作用呢?相对于html,他又有怎样的新的定义与新理念在里面呢?为什么一些专家认为html5完全完成后,所有的工作 ...
随机推荐
- position的简单用法实例 ----- 方框里图片放对应的角标
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <hea ...
- A1. JVM 内存区域
[概述] 在这篇笔记中,需要描述虚拟机中的内存是如何划分的,哪部分区域.什么样的代码和操作可能导致内存溢出异常.虽然 Java 有垃圾处理机制,但是如果生产环境在出现内存溢出异常时,由于开发人员不熟悉 ...
- layer弹出层无法关闭问题
parent.layer.closeAll();如果关闭指定弹出层,获取对应弹出层的索引,进行关闭
- [luogu4056 JSOI2009] 火星藏宝图 (贪心 dp)
传送门 Solution 一个显然的贪心:选的点数越多越好.这个随便推推就知道了. 那么我们就贪心的从一列上挑最靠下的转移 直接转移不斜率优化复杂度\(O(nm)\),吸一口O2过了... Code ...
- 【IDEA】【Git】pull代码始终无法pull到最新的代码或者提示pull no items 【解决方式】
最近pull代码老是提示pull no items,但是本地并不是最新的代码,看了各种博客始终无法解决,最后靠自己的方式解决.下面是解决方法. 方法:1.首先git --> repository ...
- linux tload-显示系统负载状况
推荐:更多linux 性能监测与优化 关注:linux命令大全 tload命令以图形化的方式输出当前系统的平均负载到指定的终端.假设不给予终端机编号,则会在执行tload指令的终端机显示负载情形. 语 ...
- 深入理解PHP之strpos
概述 在php中经常用 strpos 判断字符串是否在另一个字符串中存在, 本文介绍 strpos 函数及其实现. strpos应用 <?php /* strpos示例 */ // test e ...
- 【14】AngularJS 表单
AngularJS 表单 AngularJS 表单是输入控件的集合. HTML 控件 以下 HTML input 元素被称为 HTML 控件: input 元素 select 元素 button 元素 ...
- js中匿名函数的N种写法
匿名函数没有实际名字,也没有指针,怎么执行? 关于匿名函数写法,很发散~ +号是让函数声明转换为函数表达式.汇总一下 最常见的用法: 代码如下: (function() { alert('water ...
- mdbtools使用
1.导入数据库到mysql(将key.mdb导入MySQL的test数据库,此时只导入表结构) mdb-schema key.mdb mysql | mysql -u root -p test 2.将 ...