网易首页 > 网易号 > 正文 申请入驻

你的KVO用对了吗?

0
分享至

前言

KVO对于每一名iOS开发者而言,想必再熟悉不过了。你一定能熟练的写出KVO的日常三连:addObserverobserveValueForKeyPathremoveObserver。可是,你真的了解KVO吗?例如:KVO的底层是如何实现的?使用KVO有哪些风险?KVOController又是什么?KVOController解决了原生KVO的哪些问题,又带来了怎样的风险?

接下来,我们不妨代入到具体的场景来看问题:

场景一:Person使用KVO观察Stock的属性price。(Stock的实例对象由Person初始化,并被Person对象强持有)

下面这些问题,你能快速准确的得出答案吗?

  1. 如果使用KVC修改price属性的值,Person可以观察到price的变化吗?

  2. 如果price属性是在Stock的分类Stock+Balance中声明的,Person可以观察到price的变化吗?

  3. 如果price不是Stock的一个属性,只是Stock中一个被声明成Public的变量,Person可以观察到price的变化吗?

  4. 添加观察后,对象stock的类还是Stock吗?

  5. 当price发生变化时,消息是如何通知给Person的?

另外:

  1. KVO在iOS10及以下会出现哪些崩溃?分别是如何触发的?

  2. KVO在iOS11以以上还会出现上述6中的这些崩溃吗?

  3. KVOController会出现上述的崩溃吗?它都做了哪些优化?

  4. KVOController又有哪些坑?

如果你能快速准确的回答出上面的9个问题,那么恭喜你,你已经对KVO了如指掌,这篇文章并不是为你准备的。但是如果你对于其中的部分问题心存疑惑,那么不妨带着问题阅读完下面的内容,相信你一定可以找到答案!

本文分别从KVO的使用、实现原理和隐患三方面来展开,并在介绍完原生KVO的基础上,从源码实现的角度,介绍开源库KVOController是如何解决原生隐患的,以及其不完美之处。最后结合日常开发中可能出现的实际情况,介绍了该如何安全的使用KVOController。
什么是KVO

KVO全称为Key-Value Observing,是一种观察键值变化的机制。

回到上述的场景一:Person类的实例对象,使用KVO观察Stock类的实例对象stock的属性price。

代码实现分为三步:

  • 添加观察

@implementation Person - (void)observeNTESStock { [self.stock addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew context:nil]; } @end
  • 添加回调

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // To Do Something··· }
  • 在必要时移除观察

[self.stock removeObserver:self forKeyPath:@"price"];
各位读者可以到苹果官方文档,看到更为详细的介绍,在这里就不作过多赘述了。
KVO的实现原理

KVO相关方法的实现在Foundation框架下,由于Foundation框架是闭源的,我们无法看到最真实的源码实现。但是可以借助GNUstep窥探Foundation源码的实现。

GNUstep是GNU开源计划的项目之一,它将Cocoa的OC库,重新开源实现了一遍,虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值。

我们用简化后的图例,说明KVO的实现过程:

1. stock是Stock类的实例对象,即stock对象的isa,最初指向类对象Stock;

2. 当stock对象的price属性被KVO观察时,系统会通过runtime机制,动态地生成一个继承自Stock类的子类,并让stock对象的isa指向这个子类;

3. 在生成子类的同时,系统还会通过runtime机制,动态地为子类重写父类的一些方法,比如说:setPrice:setValue:forKeyclasssuperclass等;

setPrice:的伪代码如下:

- (void) setPrice: (int)price { // 修改price前的处理 [self willChangeValueForKey: key]; // 调用原本的setPrice方法 (*imp)(self, _cmd, val); // 修改price后的处理 [self didChangeValueForKey: key]; }

简单来说,KVO会在price的setter方法被调用时,添加一些额外的操作,来将这个变化通知给观察者。

同样setValue:forKey:方法的内部,也做了类似的处理,即在原本实现的基础上,增加向观察者同步变化的代码。

重写classsuperclass的代码如下:

- (Class) class { return class_getSuperclass(object_getClass(self)); }
- (Class) superclass { return class_getSuperclass(class_getSuperclass(object_getClass(self))); }

而重写这两个方法的目的,是为了隐藏KVO的内部实现过程。我们会发现,即使被观察对象的isa此时已经指向Stock的子类,而不是Stock,但我们通过class方法打印出的stock对象的类仍然是Stock。究其原因,就是系统重写了这两个方法,屏蔽了isa-swizzling的过程。

在介绍了主要流程后,我们用UML图,直观的介绍一下整个过程:

请特别注意:图示参考GNUstep Base中KVO的实现,具体实现可能与系统不完全一致,我们仅借此了解KVO的实现思路。图中的类名前缀GS,是GNUstep的缩写,并不是真实环境下的系统生成的类名!

  1. Person类使用KVO观察Stock类的price属性时,系统会通过Runtime动态生成继承自Stock类的子类NSKVONotifying_Stock,并重写了包括setPrice:setValue:forKey:classsuperclass在内的若干个方法。然后将Stock对象的isa,指向了新生成的NSKVONotifying_Stock。这个过程叫做isa-swizzling;

  2. 每个Stock对象还对应着一个GSKVOInfo对象,在GSKVOInfo对象中,每一个被观察的keyPath,对应着一个GSKVOPathInfo对象,GSKVOPathInfo内部又维护着一个observations数组,里面存储的是GSKVOObservation对象,GSKVOObservation对象与keyPath的观察者对象一一对应,并弱引用着观察者对象;

    我们接下来用更具象的例子,重新拆解上面的逻辑。

    比如说,对象B有两个属性name和price,对象A观察B的name,对象C观察B的name和price。

    在这个例子中,B对象关联着一个KVOInfo对象,KVOInfo对象中的Map储存着两条数据,Key分别是name和price。name对应的PathInfo中存储着一个由GSKVOObservation对象组成的数组,数组中包含两个GSKVOObservation对象,它们分别弱引用A对象和C对象。pirce对应的PathInfo中的数组包含一个弱引用C对象的GSKVOObservation对象。

    再回到我们的场景一,当price发生改变时,会先从stock对象关联的KVOInfo中找到price对应的PathInfo,再找到price属性的所有观察者,遍历并调用观察者observeValueForKeyPath:ofObject:change:context:回调通知属性的改变。

至此,我们通过GUNstep了解了KVO的实现流程。当然,GUNstep中的代码细节可能与系统不尽相同,但其实现思路还是能给我们学习KVO带来一定的启发。

KVO的隐患

在我们开发过程中,常常会遇到KVO使用不当造成的程序崩溃,这些崩溃往往是系统主动抛出的异常(NSException),下面总结了几种网上常见的几种异常类型,以及iOS10以后,此类异常是否还会出现。

异常类型

iOS10以后是否仍会出现异常

没有写观察回调方法

移除观察次数多余添加观察次数

被观察者释放前没有移除监听

不会

除了上面几种常见的崩溃外,KVO还可能出现由于多线程并发导致的崩溃。所以即便我们已经很小心谨慎,但还是难免问题的时有发生。鉴于此,我们的网易新闻工程引入了Facebook著名的开源框架KVOController。

KVOController

  • 回调更具可读性。同时支持Block、Delegate以及系统原生的observeValueForKeyPath:ofObject:change:context:

  • 更安全。不会因为移除观察而抛出异常;不会发生线程安全问题;

  • 使用更加便捷。隐式移除观察;

正是因为KVOController具备的这些特点,可以较好的解决原生KVO的几点隐患,让我们在使用KVOController进行键值观察时,避免掉一些不必要的问题。
KVOController的实现原理

仍然使用场景一来说明这个过程:这次改为Person使用FBKVO(为了区分库名与分类中命名为KVOController的对象,下文简称开源库为FBKVO)观察Stock的price属性。

代码实现:

@implementation Person - (void)observeNTESStock { [self.KVOController observe:self.stock keyPaths:@"price" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary * _Nonnull change) { // To Do Something··· }]; } @end

实现简示:

  1. Person类的实例对象会先生成一个名为KVOController的FBKVOController类的实例对象,KVOController内生成一个info对象(info内包含被观察的键值price、观察回调block等信息);

  2. KVOController会将info和被观察对象stock作为参数一同传入到单例FBKVOSharedController中,然后使用FBKVOSharedController观察stock对象,并以info作为context;

这个过程对应的UML图如下:

在NSObject (FBKVOController)的分类中,有两个不同的名字的FBKVOControler类的属性:KVOControllerKVOControllerNonRetaining,它们之间有什么相同之处,又有什么区别呢?

  1. 相同之处:

无论是KVOController还是KVOControllerNonRetaining都会与 观察者对象 强关联:它们setAssociate的Policy都是:OBJC_ASSOCIATION_RETAIN_NONATOMIC

  1. 区别:

  • KVOController强持有 被观察 的对象;

  • KVOControllerNonRetaining弱持有 被观察 的对象;

NSObject(FBKVOController)会将被观察对象作为Key(对应上图的Stock对象),一组包含_FBKVOInfo对象的Set集合作为Value存入NSMapTable中(但是为了便于理解,上图中省略了NSMapTable),KVOControllerKVOControllerNonRetaining两个的区别就在于,在NSMapTable中,KVOController中Key的类型是StrongMemory,KVOControllerNonRetaining中Key的类型是WeakMemory。

KVOController是如何解决原生隐患的?

我们再介绍下FBKVO的优势是如何实现的:

  • Notification using blocks, custom actions, or NSKeyValueObserving callback.

    FBKVO是通过单例_FBKVOSharedController来观察对象,KVO的回调也在_FBKVOSharedController中实现,在其中会依次判断block、action是否存在,如都不存在会执行原生的回调,其核心代码如下:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if (info->_block) { info->_block(observer, object, changeWithKeyPath); } else if (info->_action) { [observer performSelector:info->_action withObject:change withObject:object]; } else { [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context]; } }
  • No exceptions on observer removal.

    如一个观察者存在多次观察同一个对象的同一个键值的情况,FBKVO会在添加观察前进行判断,保证只会在第一次添加观察时生效;同理,在移除观察时,也会判断该键值是否被添加过观察,只有添加过观察才会执行removeObserver方法,从而避免了removeObserver崩溃的问题。

  • Implicit observer removal on controller dealloc.

    FBKVO会在FBKVOController对象dealloc方法中,隐式移除观察,其核心代码如下:

@FBKVOController - (void)dealloc { _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController]; for (id object in _objectInfosMap) { NSSet *infos = [objectInfoMaps objectForKey:object]; // 遍历移除所有的观察 [shareController unobserve:object infos:infos]; } } @end
  • Thread-safety with special guards against observer resurrection

    原生KVO在多线程并发下,可能存在这样的问题:

    线程一:观察者正在执行dealloc方法,并且还未执行removeObserver

    线程二:被观察对象的键值发现改变,触发了KVO的observeValueForKeyPath回调,但此时观察者已经变成野指针了

    由于在FBKVO中观察者_FBKVOSharedController是单例,不存在释放的问题,也就避免了该问题的发生。

结合上面的介绍,我们再总结下FBKVO是如何解决原生KVO的隐患的:

异常类型

KVOController处理方式

KVOController处理方式

回调写在观察者_FBKVOSharedController中

移除观察次数多余添加观察次数

根据_FBKVOInfo的keyPath,保证同一个对象的同一个键值,只会被另一个对象观察一次,并且会在移除前判断该键值是否添加过观察,只有添加过观察才会移除

被观察者释放前没有移除监听

在FBKVOController对象dealloc时,会自动移除观察

另外,由于_FBKVOSharedController是单例,永远都不会被释放,也就不会出现由于多线程并发,导致的线程安全问题。

KVOController真的完美吗?

我们的工程在使用FBKVO后,虽然避免了很多由于使用不当造成的问题,但同时也进入了一些新的问题。我们总结了两种常见的崩溃:

  1. dealloc中第一次调用KVOControllerKVOControllerNonRetaining

- (void)dealloc { [self.KVOControllerNonRetaining unobserveAll]; }

FBKVO采用了懒加载的机制,会在我们第一次调用KVOControllerKVOControllerNonRetaining时生成FBKVOController类的实例对象。一种常见的问题场景是,我们添加观察的代码是根据条件生效的,但是移除代码是不作条件区分的,在这种情况下,就可能会发生,在dealloc中第一次调用KVOController的情况,这时当运行到弱引用观察者对象这一行代码时,就会发生崩溃。

Cannot form weak reference to instance (0x600000155760) of class GCPerson. It is possible that this object was over-released, or is in the process of deallocation.

2. 在iOS10及以下系统,如果在FBKVOController释放前,被观察者对象已经被释放了,会发生崩溃;

这个问题发生的根本原因还是上文介绍的原生KVO隐患的第三条:被观察者释放前没有移除监听。但FBKVO的优点之一就是会自动移除观察,为什么还会有此类崩溃呢?要回答这个问题,我们需要重新考虑FBKVO自动移除观察的时机。

FBKVO自动移除观察的时机,是在FBKVOController的dealloc方法中!

回到场景一的例子,这次改为Person分别使用KVOController(对应下图①),以及KVOControllerNonRetaining(对应下图②)观察Stock:

如果是①这种情况,Stock对象的释放强依赖于Person对象的释放和FBKVOController对象的释放,所以能够保证在Stock对象释放前,FBKVOController对象一定执行过观察移除的操作。

但如果是②这种情况,需要考虑对象的生命周期。由于成员变量的释放是在关联对象的释放之前,在Stock对象释放时,FBKVOController的对象还未执行过移除观察的操作,就会在iOS10及以下抛出异常。

如何正确的使用KVOController

既然KVOController使用不当也会有安全隐患,那我们就该了解如何安全的使用它。

下面我们罗列了几个不同的场景,从使用KVOController还是KVOControllerNonRetaining,是否需要手动移除观察两个角度,提供一些小小的建议,仅供大家参考。

场景

(均由A观察B)

KVOController?

KVOControllerNonRetaining?

是否需要手动移除观察

注意事项

KVOController

无需手动移除

①B的生命周期> A的生命周期:KVOController

②B的生命周期< A的生命周期:KVOControllerNonRetaining

①无需手动移除

②需要手动移除

使用KVOController会导致B的释放强依赖A,假如A是单例,那么B永远不会被释放

KVOControllerNonRetaining

需要手动移除

使用KVOController会导致A与B的循环依赖

总结一下就是:

  1. 使用KVOController无需手动移除观察,使用KVOControllerNonRetaining需要在适当的时候移除观察;

  2. 一旦使用KVOController,被观察者的生命周期会受到观察者生命周期的控制。如果两者本身的生命周期互不影响,建议按照实际情况,选择使用KVOController还是KVOControllerNonRetaining;

当然,真实的业务场景往往比这复杂的多。但万变不离其宗,了解了背后的原理后不妨画一画类图,理清每个类之间的关系后,再决定使用的方式。

写在最后的话

在阅读完全篇内容后,相信你对最初的几个问题都有了答案。即使我们对KVO又爱又恨,但我们在日常开发中却总是离不开它。深入了解它,以便从最大的程度上避免问题的发生,这才是我们最应该做的。

最后预祝各位朋友,新春快乐!在新的一年里,产品都能顺利上线没有八哥。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
郑丽文访问大陆,王金平表态,马英九前智囊一句话亮了,不一般

郑丽文访问大陆,王金平表态,马英九前智囊一句话亮了,不一般

DS北风
2026-04-02 19:38:04
胡雪岩破产前的顿悟:这世上最不能得罪的不是达官显贵、地痞流氓

胡雪岩破产前的顿悟:这世上最不能得罪的不是达官显贵、地痞流氓

近史谈
2026-04-02 19:16:43
1964年毛主席得知杨育才仅是副连长,愤怒询问为何11年只升一级?

1964年毛主席得知杨育才仅是副连长,愤怒询问为何11年只升一级?

我不是沃神
2026-04-02 15:05:03
王晶再揭关之琳秘史!高尔夫球只是冰山一角,刘銮雄自爆真相太扎心

王晶再揭关之琳秘史!高尔夫球只是冰山一角,刘銮雄自爆真相太扎心

动物奇奇怪怪
2026-04-01 18:50:33
190cm硬汉!胡军儿子拒进娱乐圈,立志报国才是星二代最好的模样

190cm硬汉!胡军儿子拒进娱乐圈,立志报国才是星二代最好的模样

魔都姐姐杂谈
2026-04-02 12:22:36
张雪峰离世 10 天,11 岁女儿 3 次缅怀,字字戳心,看哭全网

张雪峰离世 10 天,11 岁女儿 3 次缅怀,字字戳心,看哭全网

魔都姐姐杂谈
2026-04-03 16:30:13
河南男子在县城买下127平房子,4年后回来结婚,打开房门他愣住了

河南男子在县城买下127平房子,4年后回来结婚,打开房门他愣住了

民间精选故事汇
2025-02-01 11:20:02
继德国之后,英国也开始贴出“中文标语”?中国游客:不能够接受

继德国之后,英国也开始贴出“中文标语”?中国游客:不能够接受

削桐作琴
2026-04-02 18:15:11
独苗王楚钦晋级8强登热搜!央视送上祝贺 7连胜小勒王皓振臂高呼

独苗王楚钦晋级8强登热搜!央视送上祝贺 7连胜小勒王皓振臂高呼

颜小白的篮球梦
2026-04-03 20:58:22
2019年,滴滴司机钟元被执行死刑,死前害怕不已,跪地不停忏悔

2019年,滴滴司机钟元被执行死刑,死前害怕不已,跪地不停忏悔

南宗历史
2026-03-17 01:08:53
日本六氟化钨拟断供,全球半导体供应链再遇冲击!

日本六氟化钨拟断供,全球半导体供应链再遇冲击!

达文西看世界
2026-04-03 14:07:40
51岁何润东直播《三角洲》火了!全程手柄操作引热议

51岁何润东直播《三角洲》火了!全程手柄操作引热议

游民星空
2026-04-03 17:18:55
他判刑13年至死未平反,10万人送行墓前立百碑,百姓说不能忘了他

他判刑13年至死未平反,10万人送行墓前立百碑,百姓说不能忘了他

小嵩
2026-04-03 14:01:17
斯普利特瑟瑟发抖!华裔新股东发话:我喜欢杨瀚森,他能成为首发

斯普利特瑟瑟发抖!华裔新股东发话:我喜欢杨瀚森,他能成为首发

球盲姐
2026-04-03 09:11:16
明里つむぎ移籍F社,到底谁才是赢家?

明里つむぎ移籍F社,到底谁才是赢家?

吃瓜党二号头目
2026-04-03 10:11:18
人民日报探访江苏、山东、河南、陕西等地:纠治乱作为,这样靶向施策

人民日报探访江苏、山东、河南、陕西等地:纠治乱作为,这样靶向施策

上观新闻
2026-04-03 07:20:03
局势再度升级!首艘开往中国的油轮遭到袭击,是误伤还是警告

局势再度升级!首艘开往中国的油轮遭到袭击,是误伤还是警告

铁锤简科
2026-04-03 15:20:07
2026天眼全覆盖!电子眼抓拍暗藏规律,牢记7点开车不扣分不踩坑

2026天眼全覆盖!电子眼抓拍暗藏规律,牢记7点开车不扣分不踩坑

复转这些年
2026-04-02 11:00:03
伊朗飘了!阿拉格奇公开吹嘘:没有哪个国家能像伊朗这样对抗美国

伊朗飘了!阿拉格奇公开吹嘘:没有哪个国家能像伊朗这样对抗美国

兴史兴谈
2026-04-02 16:15:49
张兰案终于判了!时隔3年结果大快人心,小S放肆大笑 大S遗愿难了

张兰案终于判了!时隔3年结果大快人心,小S放肆大笑 大S遗愿难了

观察鉴娱
2026-04-03 11:46:17
2026-04-03 22:24:49
网易传媒技术团队
网易传媒技术团队
网易新闻技术团队
40文章数 262关注度
往期回顾 全部

科技要闻

5万辆库存车,给了特斯拉一记重拳

头条要闻

医生成区民政局建设项目负责人 自称投资搞建设被坑了

头条要闻

医生成区民政局建设项目负责人 自称投资搞建设被坑了

体育要闻

被NBA选中20年后,他重新回到篮球场

娱乐要闻

夏克立官宣再婚当爸?否认婚内出轨

财经要闻

专家称长期摄入“飘香剂”存在健康隐患

汽车要闻

你介意和远房亲戚长得很像吗?

态度原创

本地
时尚
亲子
教育
军事航空

本地新闻

跟着歌声游安徽,听古村回响

春天不能错过的外套,这样选能穿10年

亲子要闻

孩子转运最直接的方式:爬山

教育要闻

华南理工大学2026年上海市综合评价招生简章发布

军事要闻

俄国防部:一架苏-30战机在克里米亚坠毁

无障碍浏览 进入关怀版