KVO 让人另眼相看

先配个图,看起来高级一点

KVO在OC中是实现键值(key-value-observing)观察的方式,在设计模式中是典型的观察者模式,当被观察者的键值发生改变时会通知到事先添加的观察者,在app开发中常常被使用,达到事半功倍的效果。但同时KVO在使用的过程当中有许多须要特变注意的地方,稍有不慎就会致使app崩溃,不得不让人另眼相看。究竟是怎么回事儿呢,下面根据我的的使用状况一一道来。html

使用KVO

定义2个NSObject子类对象ObjectA, ObjectB,并分别添加valueA和valueB的属性git

@interface ObjectA : NSObject
@property (nonatomic, assign) NSInteger valueA;
@end

@interface ObjectB : NSObject
@property (nonatomic, assign) NSInteger valueB;
@end

用ObjectB的对象实例objectB来观察ObjectA实例的valueA的变化,当发生变化打印对象的新值github

@implementation ObjectB

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if(![object isKindOfClass:[ObjectA class]]) {
        return;
    }
    if(![keyPath isEqualToString:@"valueA"]) {
        return;
    }
    NSLog(@"ObjectA valueA changed:%@", change);
}

@end
self.objectA = [ObjectA new];
self.objectB = [ObjectB new];
[self.objectA addObserver:self.objectB forKeyPath:@"valueA" options:NSKeyValueObservingOptionNew context:nil];
self.objectA.valueA = 20;
[self.objectA removeObserver:self.objectB forKeyPath:@"valueA"];

执行后objectA的valueA被修改成20的时候,观察者objectB会获得通知并打印其变化:设计模式

2018-11-02 10:11:08.867329+0800 KVOTestDemo[485:73437] ObjectA valueA changed:{
    kind = 1;
    new = 20;
}

KVO原理

KVO的实现是基于iOS runtime机制的isa-swizzling,当一个对象的属性被注册观察者时,会生成一个中间类继承自此类,而后将类的isa指针指向新生成的子类,这样被观察的对象就变成了这个中间类,同时重写了属性的setter方法,当新对象的属性发生变化时,则会依次通知注册的观察者对象。
苹果在这里给出了简单解释安全

注意点

重复添加观察者

连续对objectA同一属性valueA添加观察者objectB是能够的,可是也要保证在移除观察者的时候也要移除2次,否则可能会引起崩溃,由于不一样iOS系统版本表现不一致,后面会提到:app

//重复添加观察者
    self.objectA = [ObjectA new];
    self.objectB = [ObjectB new];
    [self.objectA addObserver:self.objectB forKeyPath:@"valueA" options:NSKeyValueObservingOptionNew context:nil];
    [self.objectA addObserver:self.objectB forKeyPath:@"valueA" options:NSKeyValueObservingOptionNew context:nil];
    self.objectA.valueA = 20;
    [self.objectA removeObserver:self.objectB forKeyPath:@"valueA"];
    [self.objectA removeObserver:self.objectB forKeyPath:@"valueA"];
    self.objectB = nil;
    self.objectA = nil;

观察者会被调用2次:测试

2018-11-03 16:34:08.492202+0800 KVOTestDemo[972:235154] ObjectA valueA changed:{
    kind = 1;
    new = 20;
}
2018-11-03 16:34:08.492281+0800 KVOTestDemo[972:235154] ObjectA valueA changed:{
    kind = 1;
    new = 20;
}

移除的观察者须要移除2次,否则会引起崩溃:ui

//重复添加观察者
    self.objectA = [ObjectA new];
    self.objectB = [ObjectB new];
    [self.objectA addObserver:self.objectB forKeyPath:@"valueA" options:NSKeyValueObservingOptionNew context:nil];
    [self.objectA addObserver:self.objectB forKeyPath:@"valueA" options:NSKeyValueObservingOptionNew context:nil];
    self.objectA.valueA = 20;
    [self.objectA removeObserver:self.objectB forKeyPath:@"valueA"];
//    [self.objectA removeObserver:self.objectB forKeyPath:@"valueA"];
    self.objectB = nil;
    self.objectA = nil;

在objectA销毁时由于还存在观察者而致使崩溃atom

2018-11-03 16:29:31.139120+0800 KVOTestDemo[958:233655] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x17001d720 of class ObjectA was deallocated while key value observers were still registered with it.

删除不存在的观察者

//移除不存在的观察者
    self.objectA = [ObjectA new];
    self.objectB = [ObjectB new];
    self.objectA.valueA = 20;
    [self.objectA removeObserver:self.objectB forKeyPath:@"valueA"];

objectA并无添加objectB为观察者,而直接去移除其观察者会致使崩溃。spa

2018-11-03 16:39:47.369455+0800 KVOTestDemo[979:236927] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <ObjectB 0x170017840> for the key path "valueA" from <ObjectA 0x170017830> because it is not registered as an observer.'

因此添加很删除观察者应该成对出现,互相匹配,才能保证KVO使用的正确稳定性。

被观察者销毁时还存在观察者

//被观察者销毁时还存在有未移除的观察者
    self.objectA = [ObjectA new];
    self.objectB = [ObjectB new];
    [self.objectA addObserver:self.objectB forKeyPath:@"valueA" options:NSKeyValueObservingOptionNew context:nil];
    self.objectA.valueA = 20;
    self.objectA = nil;

此例中,objectA添加了观察者objectB,可是直到objectA销毁时也没有移除此观察者,测试在iOS10及其以前系统会致使崩溃,可是iOS11后系统作了兼容,因此并不会崩溃。
iOS10上面的崩溃以下:

2018-11-03 17:05:42.101695+0800 KVOTestDemo[989:241126] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x17001f8d0 of class ObjectA was deallocated while key value observers were still registered with it.

这点值得注意,由于开发者每每在较高的iOS系统上面开发测试,而忽略了不一样版本之间的差别,或者系统覆盖测试不彻底,则可能致使APP崩溃。在ARC开发中开发者可能愈来愈少的去关注对象释放的时机,若是被观察的对象提早于观察者释放一样可能致使崩溃。

移除一个已经销毁的观察者

这种状况等同于移除一个非观察者对象,一样都会致使崩溃:

//移除一个已经销毁的观察者
    self.objectA = [ObjectA new];
    self.objectB = [ObjectB new];
    [self.objectA addObserver:self.objectB forKeyPath:@"valueA" options:NSKeyValueObservingOptionNew context:nil];
    self.objectA.valueA = 20;
    self.objectB = nil;
    [self.objectA removeObserver:self.objectB forKeyPath:@"valueA"];
    self.objectA = nil;

出现崩溃:

2018-11-03 17:11:20.322089+0800 KVOTestDemo[40637:2785015] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <(null) 0x0> for the key path "valueA" from <ObjectA 0x600002bb0d30> because it is not registered as an observer.'

因此一个对象若是做为观察者,那么在该对象dealloc前应当被移除。

总结

  • 1.KVO在使用时添加观察者和移除观察者应到成对出现
  • 2.被观察者在销毁前应当移除全部的观察者,iOS10如下会崩溃,iOS11以上不会崩溃,坑点!
  • 3.一个对象若是做为观察者,在该对象dealloc前应当被移除,不然会致使崩溃

看吧KVO真是让人另眼相看,看似功能强大,使用简单,但却暗藏杀机,稍有不慎便会致使APP崩溃,那么如何安全的使用KVO呢?
不妨试试Facebook的开源库KVOController

//FBKVOController使用起来更安全更简单
    self.objectA = [ObjectA new];
    self.objectB = [ObjectB new];
    [self.objectB.KVOController observe:self.objectA keyPath:@"valueA" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        NSLog(@"ObjectA valueA changed:%@", change);
    }];
    self.objectA.valueA = 20;
    self.objectA = nil;
    self.objectB = nil;

以上问题都迎刃而解啦!测试demo在这里Github

相关文章
相关标签/搜索