KVO 讓人刮目相看

先配個圖,看起來高級一點

KVO在OC中是實現鍵值(key-value-observing)觀察的方式,在設計模式中是典型的觀察者模式,當被觀察者的鍵值發生改變時會通知到事先添加的觀察者,在app開發中經常被使用,達到事半功倍的效果。但同時KVO在使用的過程中有許多需要特變注意的地方,稍有不慎就會導致app崩潰,不得不讓人刮目相看。到底是怎麼回事兒呢,下面根據個人的使用情況一一道來。

使用KVO

定義2個NSObject子類對象ObjectA, ObjectB,並分別添加valueA和valueB的屬性

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

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

用ObjectB的對象實例objectB來觀察ObjectA實例的valueA的變化,當發生變化打印對象的新值

@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系統版本表現不一致,後面會提到:

    //重複添加觀察者
    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次,不然會引發崩潰:

    //重複添加觀察者
    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銷燬時因爲還存在觀察者而導致崩潰

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爲觀察者,而直接去移除其觀察者會導致崩潰。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章