当前位置: 代码网 > it编程>App开发>苹果IOS > Objective-C优雅使用KVO观察属性值变化

Objective-C优雅使用KVO观察属性值变化

2024年05月19日 苹果IOS 我要评论
引言kvo 是苹果为我们提供的一套强大的机制,用于观察属性值的变化,但是大家在日常开发中想必多少也感受到了使用上的一些不便利,比如:添加观察者和移除观察者的次数需要一一对应,否则会 crash。添加观

引言

kvo 是苹果为我们提供的一套强大的机制,用于观察属性值的变化,但是大家在日常开发中想必多少也感受到了使用上的一些不便利,比如:

  • 添加观察者和移除观察者的次数需要一一对应,否则会 crash
  • 添加观察者和接受到属性变更通知的位置是分开的,不利于判断上下文。
  • 多次对同一个属性值进行观察,会触发多次回调,影响业务逻辑。

为了解决上述三个问题,业界提出了一些方便开发者的开源方案,我们一起来看一下。

kvocontroller

kvocontroller 建立在 cocoa 久经考验的 kvo 实现之上。它提供了一个简单、现代的 api,也是线程安全的。好处包括:

  • 使用 blockscustom actionsnskeyvalueobserving 回调。
  • 观察者移除没有异常。
  • 控制器 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 值,_fbkvoinfohash 方法实现如下:

- (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,这个字典以 keypathkey,与这个 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观察属性值的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com