KVO在MVC架構(gòu)的項目中是一種特別有用的技術(shù)。KVOController建立在Cocoa經(jīng)受時間考驗的KVO實現(xiàn)上。它提供簡單、現(xiàn)代的API,并且是線程安全的。優(yōu)點如下:
- 通知是通過block、action或者NSKeyValueObserving回調(diào)(即-observeValueForKeyPath:ofObject:change:context)來實現(xiàn);
- 不會出現(xiàn)移除observer的異常;
- 在controller銷毀時隱式地移除observer;
- 保證了線程安全,避免出現(xiàn)這樣的異常。
簡單地說,KVOController讓我們更優(yōu)雅、簡單、安全地使用KVO。
源碼分析
KVOController是面向觀察者設(shè)計的,而不是跟直接使用Cocoa的KVO時一樣面向被觀察者。這是一個很輕的開源庫,只由一個FBKVOController類和一個NSObject+FBKVOController分類構(gòu)成。
FBKVOController.h
FBKVOController類是用于管理整個KVO流程,它持有了觀察者對象,又提供了添加觀察行為的API,頭文件內(nèi)容如下:
初始化方法
/**
@param observer 觀察者對象
@param retainObserved 是否強引用被觀察對象
*/
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved;
/**
簡便初始化方法。
retainObserved默認為YES。
*/
- (instancetype)initWithObserver:(nullable id)observer;
公開屬性與API
/** 弱引用的方式持有觀察者對象 */
@property (nullable, nonatomic, weak, readonly) id observer;
/**
對指定對象的指定keyPath添加觀察,通過Block進行回調(diào)
@param object 被觀察對象
@param keyPath 被觀察對象的keyPath
@param options NSKeyValueObservingOptions
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
/**
以SEL的方式回調(diào)
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action;
/**
不注冊block和sel則回調(diào)觀察者類的-observeValueForKeyPath:ofObject:change:context:方法
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
/** 一次性監(jiān)聽多個keyPath */
- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options action:(SEL)action;
- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
/** 注銷觀察對象對應(yīng)的keyPath */
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath;
- (void)unobserve:(nullable id)object;
- (void)unobserveAll;
KVOController的API提供了三種回調(diào)的方式,也提供了一次性添加多個觀察的keyPaths的方法。此外,頭文件中還定義了兩個有意思的宏,用于判斷編譯時屬性是否存在(防止手滑寫錯),代碼如下:
#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))
#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))
因為我們平時直接在寫入keyPath時,都是以字符串的方式寫入,如果字符串拼寫錯誤的話可能會造成無法監(jiān)聽相應(yīng)屬性的問題。例如:
Person *person = [[Person alloc] init];
Observer *observer = [[Observer alloc] init];
[observer.KVOController observe:person
keyPath:@"fristNmae"
options:NSKeyValueObservingOptionNew
block:block];
person.firstName = @"西瓜冰";
把firstName寫錯成了fristNmae,因為屬性不存在,所以當屬性改變時沒有發(fā)生通知。使用宏FBKVOKeyPath后,我們可以跟調(diào)用對象屬性那樣將需要監(jiān)聽的屬性傳入,例如:
[observer.KVOController observe:person
keyPath:FBKVOKeyPath(person.firstName)
options:NSKeyValueObservingOptionNew
block:block];
因為有自動補全功能,所以一般不會寫錯,即使寫錯了,也會在編譯時報錯。這個宏的校驗步驟拆解后如下:
// 1 校驗傳入的KeyPath是否有編譯錯誤
((void)(NO && ((void)KEYPATH, NO))
// NO && ... 是為了運行時直接返回NO減少操作, 因為有(void)KEYPATH的存在,所以編譯時校驗了object.property
// 2 將傳入的object.property轉(zhuǎn)換為"property"
{ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; }
// 2.1 #KEYPATH將object.property轉(zhuǎn)為字符串"object.property"
// 2.2 strchr截取".property"
// 2.3 NSCAssert保證點語法的存在
// 2.4 ".property"+1="property"
// 3 使用@()語法糖將char *轉(zhuǎn)換為NSString類型
@(((void)NO, "property"))
// 因為','操作符是返回后面的值,即string = (@"a", @"b");string的值為@"b"
宏FBKVOClassKeyPath也以差不多的形式實現(xiàn),就不重復(fù)了。
FBKVOController.m
FBKVOController的實現(xiàn)文件里面包含了兩個重要的私有類_FBKVOInfo和_FBKVOSharedController。KVOController的全部功能就由這三個類來共同完成,這三個類的職責分別是:
_FBKVOInfo: 用來對每個被觀察的keyPath及對應(yīng)的options和回調(diào)(block或者SEL)進行了存儲。
FBKVOController: 將每個被觀察對象作為key值,將保存著該對象被觀察的keyPath及其對應(yīng)的回調(diào)的_FBKVOInfo的Set集合作為value值,通過一個NSMap?Table進行存儲。在添加觀察和移除觀察操作時,操作這個NSMap?Table,并且交付_FBKVOSharedController進行真正的KVO操作。
_FBKVOSharedController: 一個單例。所有的Cocoa的KVO事件都發(fā)生在這個單例對象里,這個對象是真正的觀察者。每次被監(jiān)聽對象的相關(guān)keyPath發(fā)生改變時,將會通知這個單例對象,再由這個單例對象通過_FBKVOInfo保存的信息來進行回調(diào)。
這三個對象的關(guān)系大概如上所述,接下來看看具體的代碼實現(xiàn),首先是最基礎(chǔ)的_FBKVOInfo:
屬性
@implementation _FBKVOInfo
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
/** 標志的keyPath狀態(tài),分別為_FBKVOInfoStateInitial、_FBKVOInfoStateObserving、_FBKVOInfoStateNotObserving */
_FBKVOInfoState _state;
}
_FBKVOInfo類的內(nèi)容大概就是由上面這些屬性,以及一系列初始化這些屬性的初始化方法構(gòu)成。此外還重寫了hash方法和isEqual,如下:
- (NSUInteger)hash
{
return [_keyPath hash];
}
- (BOOL)isEqual:(id)object
{
if (nil == object) {
return NO;
}
if (self == object) {
return YES;
}
if (![object isKindOfClass:[self class]]) {
return NO;
}
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}
因為一個被觀察對象的keyPath具有唯一性,為了防止對同一個對象重復(fù)添加了監(jiān)聽,所以_FBKVOInfo的唯一性由keyPath決定。
接下來是FBKVOController類,除了公開的屬性外還有以下私有屬性:
私有屬性
/** 以被觀察者對象為key,以_FBKVOInfo的Set作為value,來對其進行關(guān)聯(lián)和保存 */
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
/** 用于保證NSMapTable線程安全的鎖 */
pthread_mutex_t _lock;
這里使用NSMutableSet來保存_FBKVOInfo是為了防止重復(fù)監(jiān)聽同一個被觀察對象的同一個keyPath。NSMutableSet的唯一性是通過調(diào)用_FBKVOInfo的hash方法和isEqual方法來確定的,就如上面所述,KVOController已經(jīng)重寫了_FBKVOInfo的hash方法和isEqual方法來保證keyPath的唯一性。
使用NSMapTable而不是使用NSMutableDictionary則是因為NSMapTable能控制對key和value的內(nèi)存管理方式。
初始化方法
- (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;
}
初始化方法里,保存了觀察者對象,根據(jù)傳入的retainObserved設(shè)置NSMapTable管理內(nèi)存的方式,初始化了鎖。
接下來,以最方便的block方式回調(diào)為例,看一下KVOController的完整通知流程。
FBKVOController的方法
// 外部API接口,添加觀察的方法
- (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;
}
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
[self _observe:object info:info];
}
// 嘗試從NSMapTable中取出已保存的_FBKVOInfo對象,有則返回,無則新增,并用鎖保證了存取過程的安全。
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
pthread_mutex_lock(&_lock);
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
pthread_mutex_unlock(&_lock);
return;
}
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
[infos addObject:info];
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController] observe:object info:info];
}
該方法最后通過_FBKVOSharedController類的方法來添加真正的KVO監(jiān)聽。因為_FBKVOSharedController是個單例,所以第一次調(diào)用+sharedController會進行初始化,所以先看下_FBKVOSharedController的屬性和初始化,如下:
屬性
/** 使用NSHashTable來以弱引用的方式持有_FBKVOInfo */
NSHashTable<_FBKVOInfo *> *_infos;
/** 用于保證NSHashTable線程安全的鎖 */
pthread_mutex_t _mutex;
初始化方法
- (instancetype)init
{
self = [super init];
if (nil != self) {
NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}
#endif
pthread_mutex_init(&_mutex, NULL);
}
return self;
}
跟FBKVOController差不多的初始化方法,沒啥好說的。
_FBKVOSharedController的添加觀察方法
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
// 真正使用Cocoa的KVO添加觀察,以info作為context參數(shù)
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// 當NSKeyValueObservingOptions屬性中包含NSKeyValueObservingOptionInitial,
// 并且在回調(diào)中取消了監(jiān)聽(調(diào)用unobserve方法)可能因為沒有移除監(jiān)聽導(dǎo)致出現(xiàn)安全問題。
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
_FBKVOSharedController的KVO監(jiān)聽方法
// KVO監(jiān)聽方法
- (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;
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];
}
}
}
}
}
在這個監(jiān)聽方法中,_FBKVOSharedController將接收到的更改信息重新封裝后轉(zhuǎn)發(fā)給_FBKVOInfo保存的觀察者的對象。
從上面整個流程中,我們可以看到,觀察者對象并沒有真正地對被觀察者對象進行任何監(jiān)聽,而是通過一個專門負責觀察監(jiān)聽和轉(zhuǎn)發(fā)信息的單例類來完成監(jiān)聽和發(fā)送通知。這樣做的好處是為防止發(fā)送通知時觀察者對象未移除監(jiān)聽并且已經(jīng)不存在而導(dǎo)致應(yīng)用Crash的情況提供了雙層保障。因為該單例的在APP整個生命周期內(nèi)都存在,所以最多是接收到信息并不進行其他操作。最后,我們再看下KVOController怎么取消觀察:
FBKVOController取消觀察方法
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath
{
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath];
[self _unobserve:object info:info];
}
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
pthread_mutex_lock(&_lock);
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
_FBKVOInfo *registeredInfo = [infos member:info];
if (nil != registeredInfo) {
[infos removeObject:registeredInfo];
if (0 == infos.count) {
[_objectInfosMap removeObjectForKey:object];
}
}
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}
差不多就是添加觀察的逆過程。
_FBKVOSharedController取消觀察方法
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
if (0 == infos.count) {
return;
}
pthread_mutex_lock(&_mutex);
for (_FBKVOInfo *info in infos) {
[_infos removeObject:info];
}
pthread_mutex_unlock(&_mutex);
for (_FBKVOInfo *info in infos) {
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
}
這里取消對多個_FBKVOInfo的觀察的-unobserve:infos:方法,不是遍歷著調(diào)用-unobserve:info:,而是使用以上代碼實現(xiàn)是為了減少互斥鎖切換消耗的時間。
FBKVOController的dealloc方法
- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}
在FBKVOController對象銷毀時會移除取消所有觀察。
NSObject+FBKVOController
分類NSObject+FBKVOController進一步簡化了我們的使用,這個分類提供了兩個屬性:
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
這兩屬性都是懶加載的形式創(chuàng)建,區(qū)別在于是否強引用被觀察對象。
- (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);
}
通過動態(tài)綁定的方式保存這兩屬性。
Reference
https://github.com/facebook/KVOController
本文作者:西瓜冰soso
本文鏈接:http://m.itdecent.cn/p/8deccb9c8398
溫馨提示:
由于本文是原創(chuàng)文章,可能會有更新以及修正一些錯誤,因此轉(zhuǎn)載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導(dǎo)。另外文章如有錯誤,請不吝指教,謝謝。