您好,登錄后才能下訂單哦!
問題
KVO即:Key-Value Observing, 直譯為:基于鍵值的觀察者。 它提供一種機制,當指定的對象的屬性被修改后,則對象就會接受到通知。 簡單的說就是每次指定的被觀察的對象的屬性被修改后,KVO就會自動通知相應的觀察者了。
KVO的優點:當有屬性改變,KVO會提供自動的消息通知。 這樣開發人員不需要自己去實現這樣的方案:每次屬性改變了就發送消息通知。 這是KVO機制提供的最大的優點。 因為這個方案已經被明確定義,獲得框架級支持,可以方便地采用。 開發人員不需要添加任何代碼,不需要設計自己的觀察者模型,直接可以在工程里使用。 其次,KVO的架構非常的強大,可以很容易的支持多個觀察者觀察同一個屬性,以及相關的值。
但我們都知道, 使用KVO模式, 對某個屬性進行監聽時, Observer 需要在必要的時刻進行移除, 否則 App 必然會 Crash. 這個問題有點煩人, 因為偶爾會忘記寫移除 Observer 的代碼...
我一直想要這樣一個效果:
只管監聽, 并處理監聽方法. 不去分心, 管何時移除 Observer , 讓其能夠適時自動處理.
所幸, 它能夠實現, 先預覽一下:
@interface NSObject (SJObserverHelper) - (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; @end @interface SJObserverHelper : NSObject @property (nonatomic, unsafe_unretained) id target; @property (nonatomic, unsafe_unretained) id observer; @property (nonatomic, strong) NSString *keyPath; @property (nonatomic, weak) SJObserverHelper *factor; @end @implementation SJObserverHelper - (void)dealloc { if ( _factor ) { [_target removeObserver:_observer forKeyPath:_keyPath]; } } @end @implementation NSObject (ObserverHelper) - (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil]; SJObserverHelper *helper = [SJObserverHelper new]; SJObserverHelper *sub = [SJObserverHelper new]; sub.target = helper.target = self; sub.observer = helper.observer = observer; sub.keyPath = helper.keyPath = keyPath; helper.factor = sub; sub.factor = helper; const char *helpeKey = [NSString stringWithFormat:@"%zd", [observer hash]].UTF8String; objc_setAssociatedObject(self, helpeKey, helper, OBJC_ASSOCIATION_RETAIN_NONATOMIC); objc_setAssociatedObject(observer, helpeKey, sub, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end
項目源碼
下面來說說一步一步的實現吧:
初步思路實現:
我們都知道, 對象被釋放之前, 會調用dealloc方法, 其持有的實例變量也會被釋放.
我就這樣想, 在監聽注冊時, 為self和Observer關聯個臨時對象, 當兩者在釋放實例變量時, 我借助這個時機, 在臨時對象的dealloc方法中, 移除Observer就行了.
想法很好, 可總不能每個類里都加一個臨時對象的屬性吧. 那如何在不改變原有類的情況下, 為其關聯一個臨時對象呢?
關聯屬性
不改變原有類, 這時候肯定是要用Category了, 系統框架里面有很多的分類, 并且有很多的關聯屬性, 如下圖 UIView 頭文件第180行:
依照上圖, 我們先看一個示例, 為NSObject的添加一個Category, 并添加了一個property, 在.m中實現了它的setter和getter方法.
#import <objc/message.h> @interface NSObject (Associate) @property (nonatomic, strong) id tmpObj; @end @implementation NSObject (Associate) static const char *testKey = "TestKey"; - (void)setTmpObj:(id)tmpObj { // objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) objc_setAssociatedObject(self, testKey, tmpObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)tmpObj { // objc_getAssociatedObject(id object, const void *key) return objc_getAssociatedObject(self, testKey); } @end
很明確, objc_setAssociatedObject 便是關聯屬性的setter方法, 而objc_getAssociatedObject便是關聯屬性的getter方法. 最需要關注的就是setter方法, 因為我們要用來添加關聯屬性對象.
初步思路探索
初步嘗試:
既然屬性可以隨時使用objc_setAssociatedObject關聯了, 那我就嘗試先為self關聯一個臨時對象, 在其dealloc中, 將Observer移除.
@interface SJObserverHelper : NSObject @property (nonatomic, weak) id target; @property (nonatomic, weak) id observer; @property (nonatomic, strong) NSString *keyPath; @end @implementation SJObserverHelper - (void)dealloc { [_target removeObserver:_observer forKeyPath:_keyPath]; } @end - (void)addObserver { NSString *keyPath = @"name"; [_xiaoM addObserver:_observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil]; SJObserverHelper *helper_obj = [SJObserverHelper new]; helper_obj.target = _xiaoM; helper_obj.observer = _observer; helper_obj.keyPath = keyPath; const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String; // 關聯 objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
于是, 美滋滋的運行了一下程序, 當將_xiaoM 置為 nil 時, 砰 App Crash......
reason: 'An instance 0x12cd1c370 of class Person was deallocated while key value observers were still registered with it.
分析: 臨時對象的dealloc, 確確實實的跑了. 為什么會還有registered? 于是我嘗試在臨時對象的dealloc中, 打印實例變量target, 發現其為nil. 好吧, 這就是Crash問題原因!
嘗試 unsafe_unretained
通過上面操作, 我們知道self在被釋放之前, 會先釋放其持有的關聯屬性, self并未完全釋放, 可在臨時對象中target卻成了nil. 同時self還是有效的, 那如何保持不為nil呢?
我們看看OC中的兩個修飾符weak與unsafe_unretained:
由上, unsafe_unretained很好的解決了我們的問題. 于是我做了如下修改:
@interface SJObserverHelper : NSObject @property (nonatomic, unsafe_unretained) id target; @property (nonatomic, unsafe_unretained) id observer; @property (nonatomic, strong) NSString *keyPath; @end
再次運行程序, 還行, 觀察者移除了.
最終實現
還存在的問題
目前, 我們只是實現了, 如何在self釋放的時候, 移除自己身上的Observer.
但如果Observer提前釋放了呢?
而添加關聯屬性, 兩者還不能同時持有臨時對象, 否則臨時對象也不會及時的釋放.
好吧, 既然一個不行, 那就各自關聯一個:
- (void)addObserver { ..... SJObserverHelper *helper_obj = [SJObserverHelper new]; SJObserverHelper *sub_obj = [SJObserverHelper new]; sub_obj.target = helper_obj.target = _xiaoM; sub_obj.observer = helper_obj.observer = _observer; sub_obj.keyPath = helper_obj.keyPath = keyPath; const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String; // 關聯 objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 關聯 objc_setAssociatedObject(_observer, helpeKey, sub_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
如上, 仔細想想, 存在一個很明顯的問題, 兩個關聯屬性釋放的同時, 進行了兩次觀察移除的操作. 為避免這個問題, 我又做了如下修改:
@interface SJObserverHelper : NSObject @property (nonatomic, unsafe_unretained) id target; @property (nonatomic, unsafe_unretained) id observer; @property (nonatomic, strong) NSString *keyPath; @property (nonatomic, weak) SJObserverHelper *factor; // 1. 新增一個 weak 變量 @end @implementation SJObserverHelper - (void)dealloc { if ( _factor ) { [_target removeObserver:_observer forKeyPath:_keyPath]; } } @end - (void)addObserver { ..... SJObserverHelper *helper_obj = [SJObserverHelper new]; SJObserverHelper *sub_obj = [SJObserverHelper new]; sub_obj.target = helper_obj.target = _xiaoM; sub_obj.observer = helper_obj.observer = _observer; sub_obj.keyPath = helper_obj.keyPath = keyPath; // 2. 互相 weak 引用 helper_obj.factor = sub_obj; sub_obj.factor = helper_obj; const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String; // 關聯 objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 關聯 objc_setAssociatedObject(_observer, helpeKey, sub_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
在之前的操作中, 我們知道, weak 修飾的變量, 在目標釋放時,持有者的實例變量都會自動置為nil, 因此如上dealloc方法中, 我們只需要判斷weak引用的實例變量factor是否為空即可.
抽取
以上操作, 我們就可以解決偶爾忘記寫移除Observer的代碼了. 現在只需要把實現抽取出來, 做成一個通用的工具方法:
我新建了一個NSObject的Category, 并添加了一個方法, 如下:
然后將上述的實現進行了整合放到了.m中:
到此, 以后只需要調用- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
這個方法即可, 移除就交給臨時變量自己搞定.
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。