在iOS开发中,KVO观察是我们经常要使用到的功能,但是当我们使用系统方式去实现时,存在着不好解决的问题:
- 需要在不使用时手动移除观察,一般在dealloc中移除, 那么如果对象被循环引用了,不会进入dealloc,那么此时KVO就未移除, 这个监听也就一直在.
- 如果被观察对象在移除所有观察者之前引用计数为0,那就会奔溃。
- 系统KVO观察的回调都是在同一个代理方法中,如果观察的属性多了就需要自己做区分,比较麻烦。
- 添加了多次观察会收到多次回调,移除也需要移除多次, 如果移除的次数多于添加的次数就会奔溃。
如果我们使用FaceBook的一个开源库KVOController
就方便很多了,不需要手动移除,并且观察的回调是在对应的block中。
pod 'KVOController', '~> 1.2.0'
[self.KVOController observe:tableView keyPath:@"mj_footer.state" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary<NSString *,id> * change) {
NSLog(@"observer=%@, object=%@, change=%@", observer, object, change);
}];
从上面代码可以看到使用起来非常简单,其中KVOController
是使用分类给NBObject添加的属性,会自动初始化赋值,所以可以直接使用self.KVOController
。
一、怎么实现KVO观察结果在block中回调的?
- 观察信息存储
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block{
// 1. 创建_FBKVOInfo对象,用于记录controller、block、options、keyPath等信息的
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
[self _observe:object info:info];
}
- (void)_observe:(id)object info:(_FBKVOInfo *)info{
// 从_objectInfosMap中以object为key找出这个对象所有的被KVO观察的信息infos。
// _objectInfosMap是一个NSMapTable映射表
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// 检查infos中是否已经有info,比对的方式是isEqual:方法, _FBKVOInfo中重写了此方法,比对的是自身地址或者keypath是否一致
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {// 这个观察已经存在
return;
}
// 将info添加到infos
[infos addObject:info];
[[_FBKVOSharedController sharedController] observe:object info:info];
}
-
_FBKVOSharedController
添加KVO监听,收到KVO通知后从info中找出block调用
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info{
// register info
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
// 添加系统KVO监听,把info作为context参数
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context{
// 1. 从_infos中取出info
_FBKVOInfo *info;
{
// _infos是NSHashTable,_FBKVOSharedController的成员变量
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
// 2. 从info中取出controller
FBKVOController *controller = info->_controller;
if (nil != controller) {
// 3.从controller中取出observer、block、keypath之后把keypath放到change中,调用_block
id observer = controller.observer;
if (info->_block) {
NSDictionary<NSString *, id> *changeWithKeyPath = change;
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
二、怎么实现自动移除观察者的?
- 首先self对KVOController是强引用的(使用属性关联实现的),KVOController的observer是weak引用的。
// NSObject+FBKVOController中的属性KVOController
- (FBKVOController *)KVOController
{
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey);
// lazily create the KVOController
if (nil == controller) {
controller = [FBKVOController controllerWithObserver:self];
self.KVOController = controller;
}
return controller;
}
// FBKVOController的观察者
@property (nullable, nonatomic, weak, readonly) id observer;
-
KVOController
中的_objectInfosMap
是observer所有观察的信息集合(sets),用来避免重复添加监听的。 - 在self销毁时,系统会自动移除对
FBKVOController
的引用,引起FBKVOController
也要dealloc, 里面做的事情就是移除所有的observer相关的KVO监听,所以做到了自动移除.
- (void)dealloc{
[self unobserveAll];
}
- (void)_unobserveAll{
NSMapTable *objectInfoMaps = [_objectInfosMap copy];
[_objectInfosMap removeAllObjects];
_FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
for (id object in objectInfoMaps) {
NSSet *infos = [objectInfoMaps objectForKey:object];
[shareController unobserve:object infos:infos];
}
}
// _FBKVOSharedController中的
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos{
for (_FBKVOInfo *info in infos) {
[_infos removeObject:info];
}
// remove observer
for (_FBKVOInfo *info in infos) {
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
}
2.系统的KVO是怎么样引用观察者的,为什么对象dealloc时如果还有KVO监听者就会奔溃?
- 对象执行dealloc时,如果发现自己的isa指向的是kvo生成的子类,就会抛出这个异常NSInternalInconsistencyException,这点可以通过在观察者对象的dealloc中写下面的代码验证,(运行看效果需要在iOS12以下的设备运行)。
- 添加的观察者是assign或者_unsafe_unretain的,所以在observer销毁之后如果对象更改了属性,就发生野指针错误。
- iOS 12开始KVO不需要手动移除也不会奔溃,通知是iOS 10以下需要手动移除。
- (void)dealloc{
// 将self.Obj设置回原来的类就不会奔溃了,说明出现NSInternalInconsistencyException的原因是
//self执行dealloc后,self.Obj也会dealloc,但没有移除观察者,发现isa指向的是kvo生成的子类
NSLog(@"self.Obj的类是:%@", object_getClass(self.Obj));
object_setClass(self.Obj, [self.Obj class]);
NSLog(@"self.Obj的类是:%@", object_getClass(self.Obj));
// [self.Obj removeObserver:self forKeyPath:@"nickName"];
}
三、怎么实现避免多次收到KVO监听的?如果多次添加相同的监听是谁会收到block回调?
由一中的代码可以看到,如果对象的监听信息已经存在了,就不会再添加监听了。
_FBKVOInfo *existingInfo = [infos member:info];
- 多次添加相同的KVO监听,结果是第一次会收到回调,因为后面的监听都添加失败了. (相同的监听指的是:observer、object、keyPath都相同)
[self.KVOController observe:self.del keyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"KVOController-----111");
}];
[self.KVOController observe:self.del keyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"KVOController-----222");
}];
[self.KVOController observe:self.del keyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"KVOController-----333");
}];
结果为:
KVOController-----111
四、用RAC实现KVO观察对比起来怎么样?
等待更新..
网友评论