引言
kvo
是苹果为我们提供的一套强大的机制,用于观察属性值的变化,但是大家在日常开发中想必多少也感受到了使用上的一些不便利,比如:
- 添加观察者和移除观察者的次数需要一一对应,否则会
crash
。 - 添加观察者和接受到属性变更通知的位置是分开的,不利于判断上下文。
- 多次对同一个属性值进行观察,会触发多次回调,影响业务逻辑。
为了解决上述三个问题,业界提出了一些方便开发者的开源方案,我们一起来看一下。
kvocontroller
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
方法。
yycategories
很多时候是否引入一个第三方库不是我们业务开发能决定的,而你又想在开发时安全方便的使用 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观察属性值的资料请关注代码网其它相关文章!
发表评论