Objective-C優(yōu)雅使用KVO觀察屬性值變化
引言
KVO 是蘋(píng)果為我們提供的一套強(qiáng)大的機(jī)制,用于觀察屬性值的變化,但是大家在日常開(kāi)發(fā)中想必多少也感受到了使用上的一些不便利,比如:
- 添加觀察者和移除觀察者的次數(shù)需要一一對(duì)應(yīng),否則會(huì)
Crash。 - 添加觀察者和接受到屬性變更通知的位置是分開(kāi)的,不利于判斷上下文。
- 多次對(duì)同一個(gè)屬性值進(jìn)行觀察,會(huì)觸發(fā)多次回調(diào),影響業(yè)務(wù)邏輯。
為了解決上述三個(gè)問(wèn)題,業(yè)界提出了一些方便開(kāi)發(fā)者的開(kāi)源方案,我們一起來(lái)看一下。
KVOController
KVOController 建立在 Cocoa 久經(jīng)考驗(yàn)的 KVO 實(shí)現(xiàn)之上。它提供了一個(gè)簡(jiǎn)單、現(xiàn)代的 API,也是線程安全的。好處包括:
- 使用
blocks、custom actions或NSKeyValueObserving回調(diào)。 - 觀察者移除沒(méi)有異常。
- 控制器
dealloc時(shí)隱式移除觀察者。 - 具有防止觀察者復(fù)活的特殊保護(hù)的線程安全。
其使用方式也很簡(jiǎn)單:
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
// update clock view with new value
clockView.date = change[NSKeyValueChangeNewKey];
}];
同時(shí),KVOController 還提供了分類,通過(guò)關(guān)聯(lián)引用自動(dòng)幫你創(chuàng)建了 KVOController 框架,方便我們使用:
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];
我們來(lái)簡(jiǎn)單看一下 KVOController 是怎么做的:
- (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;
}
KVOController 分為兩種:強(qiáng)引用和弱引用,其中強(qiáng)引用會(huì)在使用時(shí)持有被觀察的對(duì)象,反之弱引用則不會(huì)。所以在初始化的時(shí)候,會(huì)創(chuàng)建一個(gè) objectInfosMap,這個(gè)是 NSMapTable,支持弱引用容器。同時(shí)會(huì)創(chuàng)建一個(gè)鎖。
注冊(cè)觀察者的時(shí)候的代碼如下:
- (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];
}
通過(guò)創(chuàng)建 _FBKVOInfo 對(duì)象,來(lái)實(shí)現(xiàn)對(duì)觀察者信息的封裝,算是一個(gè)模型類,這個(gè)內(nèi)部類的初始化方法如下:
- (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
return [self initWithController:controller keyPath:keyPath options:options block:block action:NULL context:NULL];
}
- (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;
}
接下來(lái)會(huì)將觀察者的信息存儲(chǔ)到 KVOController 創(chuàng)建時(shí)初始化的 NSMapTable 中:
- (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];
}
objectInfosMap 是一個(gè) NSMapTable 對(duì)象,使用被觀察的對(duì)象 object 作為 key, NSMutableSet 作為 value,如果已經(jīng)有 info 存在了,不會(huì)進(jìn)行二次觀察。集合存儲(chǔ)自定義對(duì)象需要判斷其 hash 值,_FBKVOInfo 的 hash 方法實(shí)現(xiàn)如下:
- (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];
}
也就是說(shuō),觀察者、被觀察者和 keyPath 構(gòu)成了觀察的唯一性。
接下來(lái)來(lái)看 _FBKVOSharedController 如何進(jìn)行的觀察:
- (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];
}
}
_FBKVOSharedController 會(huì)將 _FBKVOInfo 存儲(chǔ)到一個(gè) NSHashTable 對(duì)象中,并對(duì)其進(jìn)行 KVO。
在接受到回調(diào)時(shí)的處理如下所示:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, 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<NSString *, 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];
}
}
}
}
}
就是根據(jù)在 _FBKVOInfo 中存儲(chǔ)的信息,進(jìn)行相應(yīng)的回調(diào)。
在持有 KVOController 的對(duì)象被銷毀的時(shí)候,KVOController 也會(huì)相應(yīng)的取消對(duì)所有觀察對(duì)象的 KVO 防止出現(xiàn) Crash:
- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}
- (void)unobserveAll
{
[self _unobserveAll];
}
- (void)_unobserveAll
{
// lock
pthread_mutex_lock(&_lock);
NSMapTable *objectInfoMaps = [_objectInfosMap copy];
// clear table and map
[_objectInfosMap removeAllObjects];
// unlock
pthread_mutex_unlock(&_lock);
_FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
for (id object in objectInfoMaps) {
// unobserve each registered object and infos
NSSet *infos = [objectInfoMaps objectForKey:object];
[shareController unobserve:object infos:infos];
}
}
需要注意的是,使用 KVOController 觀察自身屬性的時(shí)候,會(huì)出現(xiàn)內(nèi)存泄露的情況,這種情況下請(qǐng)記得使用 KVOControllerNonRetaining 來(lái)進(jìn)行觀察,同時(shí)在觀察者 dealloc 的時(shí)候,調(diào)用 unobserveAll 方法。
YYCategories
很多時(shí)候是否引入一個(gè)第三方庫(kù)不是我們業(yè)務(wù)開(kāi)發(fā)能決定的,而你又想在開(kāi)發(fā)時(shí)安全方便的使用 KVO,你可以參考 YYCategories 里提供的方案來(lái)做,使用方法如下:
[self.person addObserverBlockForKeyPath:@"age" block:^(id _Nonnull obj, id _Nonnull oldVal, id _Nonnull newVal) {
NSLog(@"oldVal: %@, newVal: %@", oldVal, newVal);
}];
其實(shí)現(xiàn)原理也很簡(jiǎn)單,通過(guò)關(guān)聯(lián)對(duì)象設(shè)置一個(gè) NSMutableDictionary,這個(gè)字典以 keyPath 為 key,與這個(gè) key 有關(guān)的所有 block 組成的可變數(shù)組為 value。
// 添加 `KVO`
- (void)addObserverBlockForKeyPath:(NSString *)keyPath block:(void (^)(__weak id obj, id oldVal, id newVal))block {
if (!keyPath || !block) return;
_YYNSObjectKVOBlockTarget *target = [[_YYNSObjectKVOBlockTarget alloc] initWithBlock:block];
NSMutableDictionary *dic = [self _yy_allNSObjectObserverBlocks];
NSMutableArray *arr = dic[keyPath];
if (!arr) {
arr = [NSMutableArray new];
dic[keyPath] = arr;
}
[arr addObject:target];
[self addObserver:target forKeyPath:keyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
}
// 根據(jù) `keyPath` 移除 `KVO`
- (void)removeObserverBlocksForKeyPath:(NSString *)keyPath {
if (!keyPath) return;
NSMutableDictionary *dic = [self _yy_allNSObjectObserverBlocks];
NSMutableArray *arr = dic[keyPath];
[arr enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
[self removeObserver:obj forKeyPath:keyPath];
}];
[dic removeObjectForKey:keyPath];
}
// 移除 `KVO`
- (void)removeObserverBlocks {
NSMutableDictionary *dic = [self _yy_allNSObjectObserverBlocks];
[dic enumerateKeysAndObjectsUsingBlock: ^(NSString *key, NSArray *arr, BOOL *stop) {
[arr enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
[self removeObserver:obj forKeyPath:key];
}];
}];
[dic removeAllObjects];
}
// 獲取當(dāng)前注冊(cè)的所有 `KVO` `Block`
- (NSMutableDictionary *)_yy_allNSObjectObserverBlocks {
NSMutableDictionary *targets = objc_getAssociatedObject(self, &block_key);
if (!targets) {
targets = [NSMutableDictionary new];
objc_setAssociatedObject(self, &block_key, targets, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return targets;
}
而通知的回調(diào)則是放在 _YYNSObjectKVOBlockTarget 中的:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (!self.block) return;
BOOL isPrior = [[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];
if (isPrior) return;
NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];
if (changeKind != NSKeyValueChangeSetting) return;
id oldVal = [change objectForKey:NSKeyValueChangeOldKey];
if (oldVal == [NSNull null]) oldVal = nil;
id newVal = [change objectForKey:NSKeyValueChangeNewKey];
if (newVal == [NSNull null]) newVal = nil;
self.block(object, oldVal, newVal);
}
不過(guò)從源碼上看,還是需要自己在 dealloc 的時(shí)候移除觀察者的,不過(guò)這種方案的好處是可以多次監(jiān)聽(tīng)同一個(gè) keyPath,實(shí)現(xiàn)真正的一對(duì)多(雖然好像沒(méi)啥荷包蛋用)。
以上就是Objective-C優(yōu)雅使用KVO觀察屬性值變化的詳細(xì)內(nèi)容,更多關(guān)于Objective-C KVO觀察屬性值的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
iOS關(guān)于多張圖片上傳、地址返回順序問(wèn)題及解決方案
這篇文章主要介紹了iOS關(guān)于多張圖片上傳、地址返回順序問(wèn)題,文章給大家?guī)?lái)了三種解決方案,通過(guò)實(shí)例文字說(shuō)明相結(jié)合的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-07-07
IOS 時(shí)間和時(shí)間戳之間轉(zhuǎn)化示例
我們經(jīng)常從服務(wù)器后臺(tái)拿到時(shí)間戳的時(shí)間,以下代碼可以實(shí)現(xiàn)將時(shí)間戳轉(zhuǎn)為可讀的時(shí)間格式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-01-01
關(guān)于iOS 11的一些新特性適配實(shí)踐總結(jié)
iOS 11 為整個(gè)生態(tài)系統(tǒng)的 UI 元素帶來(lái)了一種更加大膽、動(dòng)態(tài)的新風(fēng)格。下面這篇文章主要給大家總結(jié)介紹了關(guān)于iOS 11的一些新特性適配實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-11-11

