在本文中,我们将了解到如下内容:
- 明晰KVO中的观察者和被观察者
- KVO导致崩溃的情况一览
- 破除KVO崩溃的方案
前言
KVO(Key Value Observing) 也就是键值对观察,它是iOS中观察者模式的一种实现。KVO方便了我们做很多事情,但是在提供方便的时候,同样给我们带来了麻烦-最最最烦人的崩溃问题。
本文,我们将讨论KVO导致崩溃的各种情况,以及给出解决这些崩溃问题的方案。
观察者和被观察者
在进行具体内容的讨论之前,我们先对观察者
和被观察者
这两个在KVO中的角色进行明晰。
我们看如下代码:
1. [self addObserver:self.myView forKeyPath:@"myLabel.text" options:NSKeyValueObservingOptionNew context:nil];
2. [self.myView.myLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
对于代码 1
而言,其中观察者
是self.myView
,被观察者
是self
。
对于代码 2
而言,其中观察者
是self
,被观察者
是self.myView.myLabel
。
简单来讲,在addObserver
左边的是被观察者
,在右边的是观察者
。keyPath
必须是在被观察者
中的有效路径。被观察者
的被观察的属性发生变化时,将会由观察者
中的observeValueForKeyPath:ofObject:change:context:
方法进行响应。
KVO导致崩溃的情况一览
我们现在先来把KVO导致崩溃的原因挨个撸出来,然后再想办法解决掉所有的这些问题(思路清晰,没毛病。我们解决所有问题的思路都应该是这样的)。
下面我们先列举出我们了解到的所有引起崩溃的原因:
- 添加或移除观察时,
keypath
长度为0。 - 观察者忘记写监听回调方法
observeValueForKeyPath:ofObject:change:context:
。 - 添加和移除观察的次数不匹配
- 观察者
dealloc
后没有移除监听。 - 移除未添加监听的观察者。
- 多次添加和移除观察者,但添加和移除的次数不相同。
- 观察者
- 观察者和被观察者生命周期不一致,其中一个被释放,而另一个未被释放(比如两个局部变量之间添加观察)
- 被观察者被提前释放,iOS10及以前会崩溃(笔者未能复现)。
- 观察者提前被释放,如果未移除观察,则会崩溃。
PS:对于上面列举到的各种情况,笔者在这里说明一下。观察者dealloc
后没有移除监听* 这一情况应该是在iOS9中就被修复了,但是我找不到书面证据(略显尴尬)。被观察者被提前释放,iOS10及以前会崩溃 这一情况我没有弄出来,所以不是很确定其导致崩溃的原因,本文中将不会对其进行讨论。*
破除KVO崩溃的思路
我们的目标是解决掉上述所有问题,并且要保证无侵入性。
基于这样的目的,我们有如下的思路:
- 在
NSObject
的分类中,使用Method Swizzling
拦截addObserver:forKeyPath:options:context:
和removeObserver:forKeyPath:
方法。removeObserver:forKeyPath:context:
会在判断context
是否一致之后,再调用removeObserver:forKeyPath:
移除监听。所以我们不置换removeObserver:forKeyPath:context:
方法。 - 我们创建一个
KVOProxy
作为中间者,目的是使用KVOProxy
代替对象完成所有的观察和分发通知的功能。 -
观察者
添加观察时,我们使KVOProxy
作为真正的观察者
去添加对被观察者
的观察,当被观察者
的属性值有变化时,KVOProxy
接收observeValueForKeyPath:ofObject:change:context:
,然后再根据keypath
和ofObject
两个参数去找到并通知观察者
。 -
观察者
移除观察时,我们在KVOProxy
找到需要移除的观察,再对观察进行移除。
整体思路如上,这样述说给我们的感觉很模糊,我们还是回我们最熟悉的方式:看代码。
破除KVO崩溃的实现
首先我们先定义我们会使用到的类,中间者KVOProxy
:
@interface KVOProxy : NSObject
- (void)proxy_addObserverWithProxyItem:(KVOProxyItem *)proxyItem didAddBlock:(dispatch_block_t)didAddBlock;
- (void)proxy_removeObserved:(NSObject *)observed keyPath:(NSString *)keyPath didRemoveBlock:(dispatch_block_t)didRemoveBlock;
- (void)proxy_removeAllObserver;
@end
KVOProxy
有一些方法,我们先不做解释,后续使用到时,我们再进行详细讨论。
NSObject
分类会添加类型为KVOProxy
的成员变量kvoProxy
,代码如下:
@interface NSObject (KVOProxy)
@property (nonatomic, readonly, strong) KVOProxy *kvoProxy;
@end
我们再定义NSObject
使用的用于保存通知相关信息的对象KVOProxyItem
,代码如下:
@interface KVOProxyItem : NSObject
@property (nonatomic, weak) id observed; // 弱引用被观察者,防止循环引用
@property (nonatomic, weak) id observer; // 弱引用观察者,如果观察者被释放,这里将会变为nil
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;
@property (nonatomic, assign) void *context;
- (instancetype)initWithObserver:(id)observer observed:(id)observed keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
我们在KVOProxyItem
保存了通知相关的所有属性,其中观察者
(observer
)和被观察者
(observed
)都是使用的弱引用。当观察者
或被观察者
被释放后,observeValueForKeyPath:ofObject:change:context:
如果被调起,我们可以判断是否需要发送通知到观察者
。
KVOProxy
的私有属性定义如下:
@interface KVOProxy ()
@property (nonatomic, assign) pthread_mutex_t mutex;
@property (nonatomic, strong) NSMutableDictionary *> *proxyItemMap;
@end
互斥锁mutex
,考虑到可能会在多线程中添加或移除通知,所以我们需要做一些同步操作。
数据结构proxyItemMap
,该字典中的key
是KVO
的keyPath
。字典中的value
是KVOProxyItem
组成的集合,该集合保存了观察者
的kvoProxy
是当前KVOProxyItem
对象的所有KVO
对应的KVOProxyItem
对象。
上面这段话读起来可能比较绕口(表达能力就在这里,大家担待点~~),我再对这段话做进一步的解释:
我们会在addObserver:forKeyPath:options:context:
时,创建一个KVOProxyItem
对象:
// 创建KVOProxyItem
KVOProxyItem *item = [[KVOProxyItem alloc] initWithObserver:observer observed:self keyPath:keyPath options:options context:context];
那么这个item
就是这次KVO
对应的KVOProxyItem
对象。
我们再以下面这行代码为例:
[self addObserver:self.myView forKeyPath:@"myLabel.text" options:NSKeyValueObservingOptionNew context:nil];
这行代码执行之后,会在self.myView
的kvoProxy
的proxyItemMap
中以@"myLabel.text"
为key
的集合中,添加一个这次KVO
对应的KVOProxyItem
对象。
上面说了这么多,我们还是先把相关的代码贴出来,大家一起过过眼吧。
NSObject
的代码如下:
@implementation NSObject (KVOProxy)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self kvo_swizzleSelector:@selector(addObserver:forKeyPath:options:context:)
toSelector:@selector(kvo_addObserver:forKeyPath:options:context:)];
[self kvo_swizzleSelector:@selector(removeObserver:forKeyPath:)
toSelector:@selector(kvo_removeObserver:forKeyPath:)];
});
}
/*
removeObserver:forKeyPath:context: 会在判断context是否一致之后,再调用removeObserver:forKeyPath:移除监听
*/
- (void)kvo_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if (keyPath.length <= 0) {
NSLog(@"keyPath is empty for observer:%@...", observer);
return;
}
if ([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")]) {
[self kvo_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
// 创建KVOProxyItem
KVOProxyItem *item = [[KVOProxyItem alloc] initWithObserver:observer observed:self keyPath:keyPath options:options context:context];
// 向观察者的kvoProxy添加KVOProxyItem,如果成功则在self作为被观察者添加观察者observer.kvoProxy
[observer.kvoProxy proxy_addObserverWithProxyItem:item didAddBlock:^{
[self kvo_addObserver:observer.kvoProxy forKeyPath:keyPath options:options context:context];
}];
}
- (void)kvo_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
[observer.kvoProxy proxy_removeObserved:self keyPath:keyPath didRemoveBlock:^{
[self kvo_removeObserver:self.kvoProxy forKeyPath:keyPath];
}];
}
- (KVOProxy *)kvoProxy {
id proxy = objc_getAssociatedObject(self, _cmd);
if (proxy == nil) {
proxy = [KVOProxy new];
objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return proxy;
}
@end
- 首先,在
load
中进行方法交换。 - 在
kvo_addObserver:forKeyPath:options:context:
中进行判断,如果keyPath.length <= 0
,则直接返回,避免出现添加观察时,keypath
长度为0的问题。 - 创建
KVOProxyItem
对象。 - 向
观察者
的kvoProxy
中添加KVOProxyItem
对象,在添加成功的回调中,执行真正的添加观察的操作。但是此时的观察者
已经换成了kvoProxy
。 - 在
kvo_removeObserver:forKeyPath:
中根据keyPath
和被观察者
从观察者
的kvoProxy
中移除对应的KVOProxyItem
对象。在移除成功的回调中执行真正的移除操作。
在步骤4中,我们可以避免多次添加观察。
在步骤5中,我们可以避免移除不存在的观察。
细心的我们肯定看到了这样的代码:
if ([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")]) {
[self kvo_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
这是因为我们在添加KVO
时,如果keyPath
是多级,那么系统会自动拆分成多级进行监听。我们打印了整个过程,得到如下数据:
observer:MyView - keyPath:myView.myLabel.text
observer:NSKeyValueObservance - keyPath:myLabel.text
observer:NSKeyValueObservance - keyPath:text
keyPath
逐级变化,而系统添加的后续步骤的观察者
是NSKeyValueObservance
对象,所以我们需要进行一次过滤。
KVOProxy
的代码如下:
@implementation KVOProxy
- (void)dealloc {
// 被释放前移除所有观察
[self proxy_removeAllObserver];
pthread_mutex_destroy(&(_mutex));
}
- (instancetype)init {
self = [super init];
if (self) {
pthread_mutex_init(&(_mutex), NULL);
self.proxyItemMap = @{}.mutableCopy;
}
return self;
}
#pragma mark - KVO Handle
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (keyPath.length <= 0 || object == nil) {
return;
}
[self lock];
__block KVOProxyItem *item = nil;
NSSet *set = self.proxyItemMap[keyPath];
[set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
if (object == obj.observed &&
self == [obj.observer kvoProxy]) {
*stop = YES;
item = obj;
}
}];
[self unlock];
if (item == nil) {
return;
}
// 如果观察者被提前释放,则打印错误信息
if (item.observer == nil) {
NSLog(@"observer is nil when %@ observe keyPath:%@", [item.observed class], item.keyPath);
return;
}
// 判断当前观察者是否实现了方法observeValueForKeyPath:ofObject:change:context:
// 这个地方用respondsToSelector:检测,没有用(未实现,也返回YES)
SEL selector = @selector(observeValueForKeyPath:ofObject:change:context:);
BOOL exist = NO;
unsigned int count = 0;
Method *methoList = class_copyMethodList([item.observer class], &count);
for (int i = 0; i < count; i++) {
Method method = methoList[i];
if (method_getName(method) == selector) {
exist = YES;
break;
}
}
if (!exist) {
/*
An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
*/
NSLog(@"observer:%@ can not respond observeValueForKeyPath:ofObject:change:context:", item.observer);
return;
}
// 发送事件
[item.observer observeValueForKeyPath:keyPath ofObject:object change:change context:item.context];
}
#pragma mark - Public Methods
- (void)proxy_addObserverWithProxyItem:(KVOProxyItem *)proxyItem didAddBlock:(dispatch_block_t)didAddBlock {
if (proxyItem == nil) {
return;
}
if (proxyItem.keyPath.length <= 0) {
NSLog(@"keyPath is empty for observer:%@...", proxyItem.observer);
return;
}
[self lock];
__block BOOL added = NO;
NSMutableSet *set = self.proxyItemMap[proxyItem.keyPath];
[set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
if (obj.observer == proxyItem.observer &&
obj.observed == proxyItem.observed) {
*stop = YES;
added = YES;
}
}];
if (added) {
NSLog(@"observer:%@ for keyPath:%@ is added", [proxyItem.observer class], proxyItem.keyPath);
[self unlock];
return;
}
if (set == nil) {
set = [NSMutableSet set];
[self.proxyItemMap setObject:set forKey:proxyItem.keyPath];
}
[set addObject:proxyItem];
[self unlock];
// 必须解锁之后再进行回调,否则会导致启动后屏幕不显示内容
didAddBlock();
}
- (void)proxy_removeObserved:(NSObject *)observed keyPath:(NSString *)keyPath didRemoveBlock:(dispatch_block_t)didRemoveBlock {
if (observed == nil || keyPath.length <= 0) {
return;
}
[self lock];
NSMutableSet *set = self.proxyItemMap[keyPath];
__block KVOProxyItem *item = nil;
[set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
// 这里可能因为observed已经被释放掉,导致判断出错
// 但是判断出错对逻辑不影响,因为如果要向observed发送变更通知,observed必须不为nil
if (observed == obj.observed) {
item = obj;
*stop = YES;
}
}];
if (item) {
[set removeObject:item];
didRemoveBlock();
}
[self unlock];
}
- (void)proxy_removeAllObserver {
[self.proxyItemMap enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableSet * _Nonnull obj, BOOL * _Nonnull stop) {
[obj enumerateObjectsUsingBlock:^(KVOProxyItem * _Nonnull obj, BOOL * _Nonnull stop) {
[obj.observed removeObserver:self forKeyPath:obj.keyPath];
}];
}];
}
#pragma mark - Private Methods
- (void)lock {
pthread_mutex_lock(&(_mutex));
}
- (void)unlock {
pthread_mutex_unlock(&(_mutex));
}
/// 根据指定的keyPath和observed在proxyItemMap查找KVOProxyItem
- (KVOProxyItem *)proxyItemForKeyPath:(NSString *)keyPath observed:(id)observed {
NSMutableSet *set = self.proxyItemMap[keyPath];
__block KVOProxyItem *item = nil;
[set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
if (observed == obj.observed) {
item = obj;
*stop = YES;
}
}];
return item;
}
@end
- 在
proxy_addObserverWithProxyItem:didAddBlock:
中。- 我们对
keyPath
不合法的监听进行过滤。并且如果发现有keyPath
、observer
、observed
的监听,则认为是重复添加,我们则不再添加新的监听。
- 我们对
- 在
proxy_removeObserved:keyPath:didRemoveBlock:
中。- 我们对
keyPath
、observer
、observed
进行匹配。如果发现有一致的,则移除监听。如果没有,则不做移除操作。从而避免过多移除监听而造成的崩溃。
- 我们对
- 在
dealloc
中,我们移除self
的所有监听,防止出现对象被释放,但是未移除监听的问题。 - 在
observeValueForKeyPath:ofObject:change:context:
中。- 我们
keyPath
、observer
、observed
进行匹配,只有在匹配到之后,才会进行通知的分发。
此时,当observer
或observed
为nil
时,是无法进行分发的,从而避免了observer
或observed
被填释放导致崩溃的问题。 - 在真正分发之前,我们需要判断
observer
是否实现了方法observeValueForKeyPath:ofObject:change:context:
,如果未实现,则不进行分发。从而避免其引起的崩溃问题。
- 我们
以上,就是关于KVO崩溃破除的所有解释了,文章写得可能不够清晰。但是笔者已经很努力了,如果不够好,只能请大家谅解了(手动脸红)。