前言
KVO对于每一名iOS开发者而言,想必再熟悉不过了。你一定能熟练的写出KVO的日常三连:addObserver、observeValueForKeyPath、removeObserver。可是,你真的了解KVO吗?例如:KVO的底层是如何实现的?使用KVO有哪些风险?KVOController又是什么?KVOController解决了原生KVO的哪些问题,又带来了怎样的风险?
接下来,我们不妨代入到具体的场景来看问题:
场景一:Person使用KVO观察Stock的属性price。(Stock的实例对象由Person初始化,并被Person对象强持有)
下面这些问题,你能快速准确的得出答案吗?
如果使用KVC修改price属性的值,Person可以观察到price的变化吗?
如果price属性是在Stock的分类Stock+Balance中声明的,Person可以观察到price的变化吗?
如果price不是Stock的一个属性,只是Stock中一个被声明成Public的变量,Person可以观察到price的变化吗?
添加观察后,对象stock的类还是Stock吗?
当price发生变化时,消息是如何通知给Person的?
另外:
KVO在iOS10及以下会出现哪些崩溃?分别是如何触发的?
KVO在iOS11以以上还会出现上述6中的这些崩溃吗?
KVOController会出现上述的崩溃吗?它都做了哪些优化?
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:forKey,class,superclass等;
setPrice:的伪代码如下:
- (void) setPrice: (int)price { // 修改price前的处理 [self willChangeValueForKey: key]; // 调用原本的setPrice方法 (*imp)(self, _cmd, val); // 修改price后的处理 [self didChangeValueForKey: key]; }
简单来说,KVO会在price的setter方法被调用时,添加一些额外的操作,来将这个变化通知给观察者。
同样setValue:forKey:方法的内部,也做了类似的处理,即在原本实现的基础上,增加向观察者同步变化的代码。
重写class和superclass的代码如下:
- (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的缩写,并不是真实环境下的系统生成的类名!
Person类使用KVO观察Stock类的price属性时,系统会通过Runtime动态生成继承自Stock类的子类NSKVONotifying_Stock,并重写了包括
setPrice:,setValue:forKey:,class,superclass在内的若干个方法。然后将Stock对象的isa,指向了新生成的NSKVONotifying_Stock。这个过程叫做isa-swizzling;每个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
实现简示:
Person类的实例对象会先生成一个名为KVOController的FBKVOController类的实例对象,KVOController内生成一个info对象(info内包含被观察的键值price、观察回调block等信息);
KVOController会将info和被观察对象stock作为参数一同传入到单例FBKVOSharedController中,然后使用FBKVOSharedController观察stock对象,并以info作为context;
这个过程对应的UML图如下:
在NSObject (FBKVOController)的分类中,有两个不同的名字的FBKVOControler类的属性:KVOController,KVOControllerNonRetaining,它们之间有什么相同之处,又有什么区别呢?
相同之处:
无论是KVOController还是KVOControllerNonRetaining都会与 观察者对象 强关联:它们setAssociate的Policy都是:OBJC_ASSOCIATION_RETAIN_NONATOMIC。
区别:
KVOController强持有 被观察 的对象;
KVOControllerNonRetaining弱持有 被观察 的对象;
NSObject(FBKVOController)会将被观察对象作为Key(对应上图的Stock对象),一组包含_FBKVOInfo对象的Set集合作为Value存入NSMapTable中(但是为了便于理解,上图中省略了NSMapTable),KVOController,KVOControllerNonRetaining两个的区别就在于,在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]; } } @endThread-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后,虽然避免了很多由于使用不当造成的问题,但同时也进入了一些新的问题。我们总结了两种常见的崩溃:
dealloc中第一次调用
KVOController或KVOControllerNonRetaining;
- (void)dealloc { [self.KVOControllerNonRetaining unobserveAll]; }FBKVO采用了懒加载的机制,会在我们第一次调用KVOController或KVOControllerNonRetaining时生成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的循环依赖
总结一下就是:
使用KVOController无需手动移除观察,使用KVOControllerNonRetaining需要在适当的时候移除观察;
一旦使用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.