KVO是iOS開發當中必不可少的一個工具,能夠說是使用最普遍的工具之一。不管你是要在檢測某一個屬性變化,仍是構建viewmodel雙向綁定UI以及數據,KVO都是一個十分使用的工具。html
然而!!git
KVO用起來太TMD麻煩了,要註冊成爲某個對象屬性的觀察者,要在適當的時候移除觀察者狀態,還要寫毀掉函數,更蛋疼的是對象屬性還要用字符串做爲表示。其中任何一個地方都要注意不少點,並且由於Delegate回調函數的緣由,致使代碼分離,可讀性極差,維護起來異常費勁。github
因此說,對於我來講,能不用的時候,儘可能繞過去用其餘的方法,直到我發現了Facebook的開源框架KVOController。api
在Category的.h文件中有兩個屬性,根據備註可知區別在乎一個是持有的,另外一個不是。安全
/**
@abstract Lazy-loaded FBKVOController for use with any object
@return FBKVOController associated with this object, creating one if necessary
@discussion This makes it convenient to simply create and forget a FBKVOController, and when this object gets dealloc'd, so will the associated controller and the observation info. */ @property (nonatomic, strong) FBKVOController *KVOController; /** @abstract Lazy-loaded FBKVOController for use with any object @return FBKVOController associated with this object, creating one if necessary @discussion This makes it convenient to simply create and forget a FBKVOController. Use this version when a strong reference between controller and observed object would create a retain cycle. When not retaining observed objects, special care must be taken to remove observation info prior to deallocation of the observed object. */ @property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;複製代碼
Category的.m文件和其餘文件相似,寫的都是setter以及getter方法,而且在getter方法中對別對兩個屬性作了對於 FBKVOController 的初始化。bash
- (FBKVOController *)KVOController
{
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey);
// lazily create the KVOController
if (nil == controller) {
controller = [FBKVOController controllerWithObserver:self];
self.KVOController = controller;
}
return controller;
}
- (void)setKVOController:(FBKVOController *)KVOController
{
objc_setAssociatedObject(self, NSObjectKVOControllerKey, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (FBKVOController *)KVOControllerNonRetaining
{
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
if (nil == controller) {
controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
self.KVOControllerNonRetaining = controller;
}
return controller;
}
- (void)setKVOControllerNonRetaining:(FBKVOController *)KVOControllerNonRetaining
{
objc_setAssociatedObject(self, NSObjectKVOControllerNonRetainingKey, KVOControllerNonRetaining, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}複製代碼
/**
@abstract Creates and returns an initialized KVO controller instance.
@param observer The object notified on key-value change.
@return The initialized KVO controller instance.
*/
+ (instancetype)controllerWithObserver:(nullable id)observer;
/**
@abstract Registers observer for key-value change notification.
@param object The object to observe.
@param keyPath The key path to observe.
@param options The NSKeyValueObservingOptions to use for observation.
@param block The block to execute on notification.
@discussion On key-value change, the specified block is called. In order to avoid retain loops, the block must avoid referencing the KVO controller or an owner thereof. Observing an already observed object key path or nil results in no operation.
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
/**
@abstract Registers observer for key-value change notification.
@param object The object to observe.
@param keyPath The key path to observe.
@param options The NSKeyValueObservingOptions to use for observation.
@param action The observer selector called on key-value change.
@discussion On key-value change, the observer's action selector is called. The selector provided should take the form of -propertyDidChange, - propertyDidChange: or -propertyDidChange:object:, where optional parameters delivered will be KVO change dictionary and object observed. Observing nil or observing an already observed object's key path results in no operation.
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action;
/**
@abstract Block called on key-value change notification.
@param observer The observer of the change.
@param object The object changed.
@param change The change dictionary which also includes @c FBKVONotificationKeyPathKey
*/
typedef void (^FBKVONotificationBlock)(id _Nullable observer, id object, NSDictionary<NSKeyValueChangeKey, id> *change);複製代碼
KVOController的實現須要有兩個私有的成員變量:數據結構
以及另外一個暴露在外只讀的屬性:app
在實現過程當中,做爲 KVO 的管理者,其必須持有當前對象全部與 KVO 有關的信息,而在 KVOController 中,用於存儲這個信息的數據結構就是 NSMapTable。爲了保證線程安全,須要持有pthread_mutex_t鎖,用於在操做NSMapTable時候使用。框架
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
_observer = observer;
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
pthread_mutex_init(&_lock, NULL);
}
return self;
}複製代碼
很簡單,主要工做是持有了傳進來的Observer,初始化了NSMapTable以及初始化了pthread_mutex_t鎖。
值得一提的是初始化 NSMapTable,咱們回看第二部分,在屬性的區分就在因而否是持有,根據屬性的名字也能看出,不持有的話,引用計數就不會加一。因此在初始化的時候明顯的區分就是在建立NSPointerFunctionsOptions的時候,是StrongMemory仍是WeakMemory。
經過方法+ (instancetype)controllerWithObserver:(nullable id)observer初始化的時候,默認爲持有。ide
一般狀況下咱們會使用能夠回調Block的API,可是也有少數狀況下會選擇傳遞選擇子SEL的API,咱們這裏只拿傳遞Block的方法舉例子。
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
// observe object with info
[self _observe:object info:info];
}複製代碼
在這裏傳遞進來的一些參數會被封裝成爲私有的_FBKVOInfo,那咱們來簡單看一下_FBKVOInfo的主要實現:
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
_FBKVOInfoState _state;
}
- (instancetype)initWithController:(FBKVOController *)controller
keyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
block:(nullable FBKVONotificationBlock)block
action:(nullable SEL)action
context:(nullable void *)context
{
self = [super init];
if (nil != self) {
_controller = controller;
_block = [block copy];
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}複製代碼
由此能夠看出, _FBKVOInfo的主要做用就是起到了一個相似Model同樣存儲主要數據的做用,並儲存了一個_FBKVOInfoState做爲表示當前的 KVO 狀態。
須要注意的是,成員變量都是用了@public修飾。
另外,對- (NSString *)debugDescription以及- (NSString *)debugDescription兩個方法作了重寫,方便了使用以及Debug。
以後執行了私有方法- (void)_observe:(id)object info:(_FBKVOInfo *)info
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// check for info existence
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
// observation info already exists; do not observe it again
// unlock and return
pthread_mutex_unlock(&_lock);
return;
}
// lazilly create set of infos
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
// add info and oberve
[infos addObject:info];
// unlock prior to callout
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController] observe:object info:info];
}複製代碼
1)首先先進行的是對於自身持有的 _objectInfosMap這個成員變量的操做,一切都須要在先鎖定,執行結束再解鎖的過程。
2)而後是獲取了 _FBKVOSharedController單例而且執行了單例的- (void)observe:(id)object info:(nullable _FBKVOInfo *)info方法。
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
// add observer
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}複製代碼
加鎖,對於當前單例的NSHashTable進行添加操做的信息,並執行Foundation的
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;複製代碼
而後對信息中的state進行更改。
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
// lookup context in registered infos, taking out a strong reference only if it exists
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
if (nil != info) {
// take strong reference to controller
FBKVOController *controller = info->_controller;
if (nil != controller) {
// take strong reference to observer
id observer = controller.observer;
if (nil != observer) {
// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}複製代碼
這個就相對簡單了,主要是根據關注信息內是Block仍是Action來執行,若是二者都沒有就會調用觀察者 KVO 回調方法。
事實上,註銷是在執行dealloc的時候執行的,同時也去掉了鎖:
- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}複製代碼
由於KVO事件都由私有的 _KVOSharedController 來處理,因此當每個 KVOController 對象被釋放時,都會將它本身持有的全部 KVO 的觀察者交由 _KVOSharedControlle r的方法處理,咱們再來看下代碼:
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
if (0 == infos.count) {
return;
}
// unregister info
pthread_mutex_lock(&_mutex);
for (_FBKVOInfo *info in infos) {
[_infos removeObject:info];
}
pthread_mutex_unlock(&_mutex);
// remove observer
for (_FBKVOInfo *info in infos) {
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
}複製代碼
該方法會遍歷全部傳入的 _FBKVOInfo ,從其中取出keyPath 並將 _KVOSharedController 移除觀察者。
固然,假如你須要手動的移除某一個的觀察者, _KVOSharedController 也提供了方法:
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// unregister info
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);
// remove observer
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}複製代碼
這套框架提供了豐富的結構,基本可以知足咱們對於KVO的使用需求。
只須要一次代碼,就能夠完成對一個對象的鍵值觀測,同時不須要處理移除觀察者,也能夠在同一處代碼進行鍵值變化以後的處理,從噁心的回調方法中解脫出來,不只提供了使用方便,也不須要咱們手動主要觀察者,避免了各類問題,絕對算的上一個完善好用的框架。