1、KVO介紹app
KVO(Key-Value Observing),鍵值監聽。它提供一種機制:指定的被觀察者的屬性被改變後,KVO就會通知觀察者,觀察者能夠作出響應。函數
KVO做用:利用KVO,很容易實現視圖組件和數據模型的分離。當數據模型的屬性值改變以後,做爲監聽者的視圖組件就會被激發。這有利於業務邏輯和視圖展現的解耦合。this
KVO使用步驟:(1)註冊觀察,添加觀察者及屬性;(2)實現回調方法;(3)移除觀察。atom
(1)註冊觀察:spa
- (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)實現回調方法:3d
- (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 /* 注意:不要忘記解除註冊,不然會致使資源泄露 . */
2、KVO使用舉例及注意事項code
//被觀察者 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]; }
注意:orm
(1)在第二步回調observeValueForKeyPath:函數中,要用else進行判斷調用super的對應函數。由於若當前函數沒法處理對應的kvo,有可能super-class會有一些kvo的對應處理。server
(2)在第三步在dealloc函數中註銷觀察中,當對同一個keypath進行兩次removeObserver時會致使程序crash,這種狀況經常出如今父類有一個kvo,父類在dealloc中remove了一次,子類又remove了一次的狀況下。能夠利用context字段來標識出到底kvo是superClass註冊的,仍是self註冊的。咱們能夠分別在父類以及本類中定義各自的context字符串,而後在dealloc中remove observer時指定移除的自身添加的observer。這樣就能避免二次remove形成crash。
3、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解決方案:
方案1、
可讓被觀察對象持有一個KVO的delegate,全部和KVO相關的操做均經過delegate來進行管理,delegate經過創建一張map來維護KVO整個關係。
中間層delegate的代理工做:
(1)若是出現KVO重複添加觀察者或者重複移除觀察者(KVO註冊觀察者與移除觀察者不匹配)的狀況,delegate能夠直接阻止這些非正常的操做。
(2)被觀察者dealloc以前,能夠經過delegate自動將與本身有關的KVO關係都註銷掉,避免了KVO的被觀察者dealloc時仍然註冊着KVO致使的crash。
方案2、
咱們可讓觀察者在註冊的過程當中,將註冊信息一同記錄下來,而後使用某種方法在對象dealloc時,在記錄的信息裏找到對應的觀察者,註銷觀察。
此方案在宿主釋放過程當中嵌入咱們本身的對象,使得宿主釋放時順帶將咱們的對象一塊兒釋放掉,從而獲取dealloc的時機點。採用構建一個釋放通知對象,經過AssociatedObject方式鏈接到宿主對象,在宿主釋放時進行回調,完成註銷動做。
具體的原理和代碼能夠參照上一篇文章《[crash詳解與防禦] NSNotification crash》。