<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
KVO
是蘋果為我們提供的一套強大的機制,用於觀察屬性值的變化,但是大家在日常開發中想必多少也感受到了使用上的一些不便利,比如:
Crash
。為了解決上述三個問題,業界提出了一些方便開發者的開源方案,我們一起來看一下。
KVOController
建立在 Cocoa
久經考驗的 KVO
實現之上。它提供了一個簡單、現代的 API
,也是執行緒安全的。好處包括:
blocks
、custom actions
或 NSKeyValueObserving
回撥。dealloc
時隱式移除觀察者。其使用方式也很簡單:
// 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]; }];
同時,KVOController
還提供了分類,通過關聯參照自動幫你建立了 KVOController
框架,方便我們使用:
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];
我們來簡單看一下 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
分為兩種:強參照和弱參照,其中強參照會在使用時持有被觀察的物件,反之弱參照則不會。所以在初始化的時候,會建立一個 objectInfosMap
,這個是 NSMapTable
,支援弱參照容器。同時會建立一個鎖。
註冊觀察者的時候的程式碼如下:
- (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
物件,來實現對觀察者資訊的封裝,算是一個模型類,這個內部類的初始化方法如下:
- (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; }
接下來會將觀察者的資訊儲存到 KVOController
建立時初始化的 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
是一個 NSMapTable
物件,使用被觀察的物件 object
作為 key
, NSMutableSet
作為 value
,如果已經有 info
存在了,不會進行二次觀察。集合儲存自定義物件需要判斷其 hash
值,_FBKVOInfo
的 hash
方法實現如下:
- (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
構成了觀察的唯一性。
接下來來看 _FBKVOSharedController
如何進行的觀察:
- (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
會將 _FBKVOInfo
儲存到一個 NSHashTable
物件中,並對其進行 KVO
。
在接受到回撥時的處理如下所示:
- (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]; } } } } }
就是根據在 _FBKVOInfo
中儲存的資訊,進行相應的回撥。
在持有 KVOController
的物件被銷燬的時候,KVOController
也會相應的取消對所有觀察物件的 KVO
防止出現 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
觀察自身屬性的時候,會出現記憶體洩露的情況,這種情況下請記得使用 KVOControllerNonRetaining
來進行觀察,同時在觀察者 dealloc 的時候,呼叫 unobserveAll
方法。
很多時候是否引入一個第三方庫不是我們業務開發能決定的,而你又想在開發時安全方便的使用 KVO
,你可以參考 YYCategories
裡提供的方案來做,使用方法如下:
[self.person addObserverBlockForKeyPath:@"age" block:^(id _Nonnull obj, id _Nonnull oldVal, id _Nonnull newVal) { NSLog(@"oldVal: %@, newVal: %@", oldVal, newVal); }];
其實現原理也很簡單,通過關聯物件設定一個 NSMutableDictionary
,這個字典以 keyPath
為 key
,與這個 key
有關的所有 block
組成的可變陣列為 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]; } // 根據 `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]; } // 獲取當前註冊的所有 `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; }
而通知的回撥則是放在 _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); }
不過從原始碼上看,還是需要自己在 dealloc
的時候移除觀察者的,不過這種方案的好處是可以多次監聽同一個 keyPath
,實現真正的一對多(雖然好像沒啥荷包蛋用)。
以上就是Objective-C優雅使用KVO觀察屬性值變化的詳細內容,更多關於Objective-C KVO觀察屬性值的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45