[crash詳解與防護] KVO crash
一、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