入门篇

KVO是什么?

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

KVO 是 Objective-C 对观察者模式(Observer Pattern)的实现。也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知,并且得知此时这个属性的具体值。

有点绕口。简单点说,比如你监视你老婆,你老婆跟老王跑了,你会立即知道你老婆是跟老王跑了,并且也会知道上次是跟张三跑的。

KVO如何使用?

最好的使用入门指南:Introduction to Key-Value Observing Programming Guide

基础篇

KVO实现原理 – In Apple Way

苹果的实现方式用猿哥这张经典的图理解起来还不错,但是可能不太适合新手。

KVO的实现是经典的添加中间层的思想,虽然用了isa-swizzling,但是很符合逻辑,并没有什么黑魔法。我们来个生活中的实例讲解下:

我们假设有一个叫Foo的女孩,有一个叫Bar的小伙。最初他俩素未谋面:

有一天小伙Bar去网吧打撸,碰巧Foo也坐在旁边撸。Bar觉得Foo很漂亮,感觉喜欢上了Foo。于是他邀请Foo一起撸并趁机要了Foo妹子的微信,并说以后带你飞。其实Bar是为了观察Foo妹子的票圈,随时点赞评论,说不定还有机会趁机表白。于是世界因为Bar的艳遇而运行了一段代码:

1
2
3
4
5
6
7
8
9
10
- (void)registerAsObserver {
    /*
     当girlFoo的wechat属性改变时通知boyBar
     */
    [girlFoo addObserver:boyBar
             forKeyPath:@"wechat"
                 options:(NSKeyValueObservingOptionNew |
                            NSKeyValueObservingOptionOld)
                    context:NULL];
}

然后在地球上,他俩关系就变了:(画风陡变,前方高能= =)

Foo因为被Bar关注,所以她一定不再是原来那个美女,而是和以前不一样的美女(note:还是继承了原来美女的属性和方法)。她还是拥有原来的容颜和生活习惯,但是现在她在微信上的一举一动都会被Bar所察觉到了。

现在女孩回了家,发了一条微信,内容是“哎,好想找个伴”。在最后

1
[self didChangeValueForKey:@"wechat"];

执行结束的时候小伙就会看到这条消息。

你看KVO的实现方式不是很符合逻辑吗。

有兴趣深入苹果实现方式的同学就看顾神这篇文章吧《如何自己动手实现 KVO》。他的思路是比较官方化的,只是回调用的block。

这里介绍他代码里runtime中关键点实现函数原型:

1.新类被创建并继承自父类:

1
objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

2.在新类上重写setXXX方法

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

3.女孩Foo的isa指向新类

1
Class object_setClass(id object, Class cls)

进阶篇

假如面试官问你KVO是怎么实现的,你如上回答可以勉强过关,但是你说出下面的点那就能让他十分满意了,说什么?吐槽。

KVO的缺点

AFNetworking作者Mattt Thompson在《Key-Value Observing》中说:

Ask anyone who’s been around the NSBlock a few times: Key-Value Observing has the worst API in all of Cocoa.

缺点1:所有的observe处理都放在一个方法里

假设我们要观察一个tableView的contentSize,看上去使用KVO是个不错的选择,因为没有其他方法去获知这个属性被改变。Ok,首先,添加观察者:

1
[_tableView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];

很好,实现属性被改变的响应:

1
2
3
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    [self configureView];
}

完成!Just kidding.考虑到该方法中可能有其他的observe事务,所以我们要这样修改:

1
2
3
4
5
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) {
        [self configureView];
    }
}

如果KVO处理的事务繁多,就会造成该方法代码特别长,并且十分不优雅,我们还没辙。

缺点2:严重依赖于string

KVO严重依赖于string,换句话说如果KVO的keyPath如果错误编译器无法查出,比如我们可能会把@“contentSize”写成@“contentsize”,然后你就只能傻傻的找半个小时bug。

因此我们不得不使用NSStringFromSelector(@selector(contentSize))编译器就能判断是否存在这个属性,并且我们也好修改,但是代码这么长,逼死强迫症。

而且,我们也不能用KVO观察多值路径,比如我们观察一个viewController并且想获得scrollView的contentOffset,我们是不能用这样的keyPath:scrollview.contentOffset来得到的。

因此上面的代码又得变成这样:

1
2
3
4
5
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == _tableView && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {
        [self configureView];
    }
}

缺点3:需要自己处理superclass的observe事务

对于Objective-C,很多时候runtime系统都会自动帮忙处理superClass的方法,比如dealloc。在ARC下,调用子类的dealloc方法,runtime会自动调用父类的dealloc。但是KVO不会,或者说是不行,因为runtime并不知道父类有没有observe事务,并且由于它是用协议实现的,一次只能触发一个observeValueForKeyPath:ofObject:change:context:方法,所以如果子类和父类都有observe事务,我们要这样做:

1
2
3
4
5
6
7
8
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) {
        [self configureView];
    else {
    // 如果我们忘记这句,那么父类不会收到通知
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

缺点4:多次相同KVO的removeObserve会导致crash

写过KVO代码的人都知道,对同一个对象执行两次removeObserver操作会导致程序crash。

在同一个文件中执行两次相同的removeObserver属于粗心,比较容易debug出来;但是跨文件执行两次相同的removeObserver就不是那么容易发现了。

我们一般会在dealloc中进行removeObserver操作(这也是Apple所推荐的)。

思考这样一个场景:一个JZTableView继承自UITableview,他们都监听了tableView的contentSize属性,那么UItableview和JZTableView的dealloc都会有这句代码:

1
2
3
4
5
- (void)dealloc {
     ...
    [_tableView removeObserver:self forKeyPath:@"contentSize" context:NULL];
    ...
}

那么当JZTableview的对象释放时,他和她父类的dealloc都会被调用,两次相同的removeObserve,自然就Crash了。

还有很多点,不过说出上面4个就差不多了。

那么如何改进?

首先列举缺点:

  • 所有的observe处理都放在一个方法里

  • 严重依赖于string

  • 需要自己处理superclass的observe事务

  • 多次相同KVO的removeObserve会导致crash

然后直接上肖哥的一篇博客:一句代码,更加优雅的调用KVO和通知

note:以下内容有兴趣读源码的同学可以看看,否则直接看下一小节吧。

他的代码架构图如下:

每个被观察的对象有一个自己的字典,key为被观察的keyPath,存的值为一个真正的观察者对象Target,即dict[keyPath] = target。一个keyPath对应一个target,每个target有一个KVOBlockSet存放该keyPath下的所有Block。

1
2
3
4
//监听_objA的name属性
[_objA xw_addObserverBlockForKeyPath:@"name" block:^(id obj, id oldVal, id newVal) {
    NSLog(@"kvo,修改name为%@", newVal);
}];

缺陷处理结果分析:

所有的observe处理都放在一个方法里。解决。因为回调被分散成块了,不再集中。顾神也是用的块实现的回调。但是用块也不是很统一,但个人觉得还是比原生的好点。

严重依赖于string。未解决。

需要自己处理superclass的observe事务。解决。所有执行了xw_addObserverBlockForKeyPath的块都会被放入KVOBlockSet,对应的keyPath值被改变时会回调对应的所有block,不存在调用super的问题。

多次相同KVO的removeObserve会导致crash。作者用的hook dealloc调剂remove方法达到自动移除功能,目前来看逻辑没问题。

THE END

上文说道Foo妹纸发了一条微信“哎,好想找个伴”。Bar小伙见时机成熟,微信上大胆表白“你看我风流倜傥,随手一敲就是一个十位数内计算器App,你看我做你的鸳鸯咋样?”,Foo妹纸沉默了10分钟回答道:“对不起,我可能更喜欢前端工程师”。

1
2
3
- (void)resignObserver {
    [girlFoo removeObserver:boyBar forKeyPath:@"wechat"];
}

关于KVO导读的更多相关文章

  1. iOS---观察者模式之--->KVO

    文章结构如下: Why? (为什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) More (更多细节) 原理 自己实现KVO 在我的上一篇文章浅谈 iOS Notifica ...

  2. Objective-C之KVC、KVO

    1,KVC(键值编码)  Key Value Coding 1.1在C#中,可以通过字符串反射来获取对象,从而对对象的属性进行读写,Object-C中有同样的实现,通过字符串(属性名词)对对象的属性进 ...

  3. OS 如何选择delegate、notification、KVO?

    原文链接:http://blog.csdn.net/dqjyong/article/details/7685933 前面分别讲了delegate.notification和KVO的实现原理,以及实际使 ...

  4. KVC 和 KVO

    KVC 键值编码    全称是Key-value coding,翻译成键值编码.它提供了一种使用字符串而不是访问器方法去访问一个对象实例变量的机制.        1.通过key(成员变量的名称)设置 ...

  5. 11. KVC And KVO

    1. KVC And KVO  的认识 KVC/KVO是观察者模式的一种实现  KVC全称是Key-value coding,翻译成键值编码.顾名思义,在某种程度上跟map的关系匪浅.它提供了一种使用 ...

  6. KVO __ 浅谈

    KVO :Key-Value Observing 它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知.简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了. ...

  7. iOS开发系列--Objective-C之KVC、KVO

    概述 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby.Python的动态特性,例如动态类型.动态加载.动态绑定等.今天我们着重介绍ObjC中的键值编码(KVC).键值监听( ...

  8. delegate、notification、KVO场景差别

    delegate: 编译器会给出没有实现代理方法的警告 一对一 使用weak而不是assign,或者vc消失时置为nil 可以传递参数,还可以接收返回值 notification: 编译期无法排错 一 ...

  9. IOS学习之初识KVO

    什么是KVO? KVO(Key-Value Observing)键值观察,是一种通过对对象的某一个属性添加观察者,一旦这个属性值发生变化,就会通知当前观察者的一种机制. 该如何使用? 1.注册,指定被 ...

随机推荐

  1. 如何通过navigator.userAgent判断是哪款浏览器?

    userAgent 用户代理.通过浏览器控制台alert( navigator.userAgent );可以获得当前浏览器的信息,如果逆推呢? 通过navigator.userAgent判断是哪款浏览 ...

  2. Linux开机最简化

    [root@localhost ~]# LANG=en [root@localhost ~]# for root in chkconfig --list|grep 3:on|awk '{print $ ...

  3. 201521123069 《Java程序设计》 第3周学习总结

    1. 本章学习总结 如果看不清楚可点击类与对象 2. 书面作业 Q1. 代码阅读 public class Test1 { private int i = 1;//这行不能修改 private sta ...

  4. 201521123045 《JAVA程序设计》 第14周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多数据库相关内容. 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自 ...

  5. 201521123048 《Java程序设计》第14周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多数据库相关内容. 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自 ...

  6. 201521123068 《java程序设计》 第10周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常与多线程相关内容. 2. 书面作业 本次PTA作业题集异常.多线程 1.finally 题目4-2 1.1 截图你的提交结果(出 ...

  7. 201521123070《Java程序设计》 第11周学习总结

    本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 书面作业 本次PTA作业题集多线程 互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问) 1.1 除了使用 ...

  8. ORACLE PROC开发(转载)

    Proc也就是嵌入式C,与informix的ESQ/C有类似之处,本部分主要列出Proc与Esql的区别,相同部分请参见informix部分. 1.数组功能 Proc中支持使用宿主变量数组一次查询SE ...

  9. 在linux下通过hexdump生成一个十六进制的文本保存文件,解析此文件转变成正常源代码文件。

    举例说明: 此十六进制保存的文件为此源代码hexdump生成的: #include<stdio.h> #include<string.h> #include<stdlib ...

  10. pyhton之路---面向对象

    一.面向过程VS面向对象 面向过程:      优点:极大的降低了写程序的复杂度,只需要顺着执行的步骤,堆叠代码即可.      缺点:一套流水线或者流程就是来解决一个问题,代码就是牵一发而动全身 面 ...