1. 程式人生 > >[crash詳解與防護] KVO crash

[crash詳解與防護] KVO crash

eval nat new mat not 自身 init 步驟 Coding

一、KVO介紹

KVO(Key-Value Observing),鍵值監聽。它提供一種機制:指定的被觀察者的屬性被改變後,KVO就會通知觀察者,觀察者可以做出響應。

  KVO作用:利用KVO,很容易實現視圖組件和數據模型的分離。當數據模型的屬性值改變之後,作為監聽者的視圖組件就會被激發。這有利於業務邏輯和視圖展示的解耦合。

KVO使用步驟:(1)註冊觀察,添加觀察者及屬性;(2)實現回調方法;(3)移除觀察。

(1)註冊觀察:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void
*)context /* observer:觀察者,也就是KVO通知的訂閱者。訂閱著必須實現observeValueForKeyPath:ofObject:change:context:方法   keyPath:描述將要觀察的屬性,相對於被觀察者。   options:KVO的一些屬性配置;有四個選項。     options所包括的內容:       NSKeyValueObservingOptionNew:change字典包括改變後的值;       NSKeyValueObservingOptionOld: change字典包括改變前的值;       NSKeyValueObservingOptionInitial:註冊後立刻觸發KVO通知;       NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次).   context: 上下文,這個會傳遞到訂閱著的函數中,用來區分消息,所以應當是不同的。
*/

(2)實現回調方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
  /*
    keyPath:被監聽的keyPath , 用來區分不同的KVO監聽. 
     object: 被觀察修改後的對象(可以通過object獲得修改後的值). 
     change:保存信息改變的字典(可能有舊的值,新的值等) .
     context:上下文,用來區分不同的KVO監聽.
*/

(3)移除觀察

 - (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath
  - (void)removeObserver:(NSObject *)observer     forKeyPath:(NSString *)keyPath  context:(void *)context
 /*
     註意:不要忘記解除註冊,否則會導致資源泄露 .
 */

二、KVO使用舉例及註意事項

//被觀察者 StockData.m
#import "StockData.h"
@interface StockData()
@property(nonatomic, strong)NSString *stockName;
@property(nonatomic, strong)NSString *price;
@end

//觀察者 SLVKVOController.m
#import "SLVKVOController.h"
#import "StockData.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.stockData setValue:@"searph" forKey:@"stockName"];
    [self.stockData setValue:@"10.0" forKey:@"price"];
    [self.stockData addObserver:self forKeyPath:@"price"  options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:SLVKVOContext];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if(context == SLVKVOContext && object == self.stockData && [keyPath isEqualToString:@"price"]) {
        NSString * oldValue = [change objectForKey:NSKeyValueChangeOldKey];
        NSString * newValue = [change objectForKey:NSKeyValueChangeNewKey];
        self.myLabel.text = [NSString stringWithFormat:@"oldValue:%@ , newValue:%@",oldValue,newValue];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

-(void)dealloc {
    [self.stockData removeObserver:self forKeyPath:@"price" context:SLVKVOContext];
}

註意:

(1)在第二步回調observeValueForKeyPath:函數中,要用else進行判斷調用super的對應函數。因為若當前函數無法處理對應的kvo,有可能super-class會有一些kvo的對應處理。

(2)在第三步在dealloc函數中註銷觀察中,當對同一個keypath進行兩次removeObserver時會導致程序crash,這種情況常常出現在父類有一個kvo,父類在dealloc中remove了一次,子類又remove了一次的情況下。可以利用context字段來標識出到底kvo是superClass註冊的,還是self註冊的。我們可以分別在父類以及本類中定義各自的context字符串,然後在dealloc中remove observer時指定移除的自身添加的observer。這樣就能避免二次remove造成crash。

三、KVO常見crash及防護方案

KVO常見crash類型:

(1)不能對不存在的屬性進行kvo觀測,否則會報crash:uncaught exception ‘NSUnknownKeyException‘, reason: ‘[<StockData 0x600000203d50> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key stockName.‘

(2)訂閱者必須實現 observeValueForKeyPath:ofObject:change:context:方法,否則crash。

Terminating app due to uncaught exception ‘NSInternalInconsistencyException‘, reason: ‘<SLVKVOController: 0x7f811372ff70>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.

(3) 移除觀察,超過addObserver的次數就會 crash:Terminating app due to uncaught exception ‘NSRangeException‘, reason: ‘Cannot remove an observer <SLVKVOController 0x7ff8e8703100> for the key path "price" from <StockData 0x60800003d000> because it is not registered as an observer.‘

KVO crash解決方案:

方案一、

  可以讓被觀察對象持有一個KVO的delegate,所有和KVO相關的操作均通過delegate來進行管理,delegate通過建立一張map來維護KVO整個關系。

中間層delegate的代理工作:

(1)如果出現KVO重復添加觀察者或者重復移除觀察者(KVO註冊觀察者與移除觀察者不匹配)的情況,delegate可以直接阻止這些非正常的操作。

(2)被觀察者dealloc之前,可以通過delegate自動將與自己有關的KVO關系都註銷掉,避免了KVO的被觀察者dealloc時仍然註冊著KVO導致的crash。

方案二、

  我們可以讓觀察者在註冊的過程中,將註冊信息一同記錄下來,然後使用某種方法在對象dealloc時,在記錄的信息裏找到對應的觀察者,註銷觀察。

  此方案在宿主釋放過程中嵌入我們自己的對象,使得宿主釋放時順帶將我們的對象一起釋放掉,從而獲取dealloc的時機點。采用構建一個釋放通知對象,通過AssociatedObject方式連接到宿主對象,在宿主釋放時進行回調,完成註銷動作。

具體的原理和代碼可以參照上一篇文章《[crash詳解與防護] NSNotification crash》。

[crash詳解與防護] KVO crash