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

目录
  • 引言
  • KVOController
  • YYCategories

引言

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观察属性值的资料请关注我们其它相关文章!

时间: 2022-08-08

iOS 监听回调机制KVO实例

监听某个对象,如果这个对象的数据发生变化,会发送给监听者从而触发回调函数 [self.bean addObserver:self forKeyPath:@"data" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL]; 这个就是注册监听,这个@"data"作为标识符方便回调函数辨认 -(void)observeValueForKeyPath:(NSStrin

在Swift中使用KVO的细节以及内部实现解析(推荐)

KVO是什么? KVO 是 Objective-C 对观察者设计模式的一种实现.[另外一种是:通知机制(notification),详情参考:iOS 趣谈设计模式--通知]: KVO提供一种机制,指定一个被观察对象(例如A类),当对象某个属性(例如A中的字符串name)发生更改时,对象会获得通知,并作出相应处理:[且不需要给被观察的对象添加任何额外代码,就能使用KVO机制] 在MVC设计架构下的项目,KVO机制很适合实现mode模型和view视图之间的通讯. 例如:代码中,在模型类A创建属性数据

Objective-C&nbsp;const常量的优雅使用方法

目录 正文 Objective-C 的常量声明方式 在 Objective-C 中使用 let 来声明常量 正文 在编写代码时经常要使用常量,来替代 magic number.比较简单的做法是通过预处理指令 #define 来实现. #define ANIMATION_DURATION 0.3 上述预处理指令会在编译时的预处理阶段会将代码中 ANIMATION_DURATION 字符串替换为 0.3.这种定义常量的方式比较简便,但是存在两个问题: 丢失了类型信息. 若该预处理指令声明在头文件中,

iOS自动移除KVO观察者的实现方法

问题 KVO即:Key-Value Observing, 直译为:基于键值的观察者. 它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知. 简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了. KVO的优点:当有属性改变,KVO会提供自动的消息通知. 这样开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知. 这是KVO机制提供的最大的优点. 因为这个方案已经被明确定义,获得框架级支持,可以方便地采用. 开发人员不需要添加任何代码,不需要

Objective-C之Category实现分类示例详解

目录 引言 编译时 运行时 引言 在写 Objective-C 代码的时候,如果想给没法获得源码的类增加一些方法,Category 即分类是一种很好的方法,本文将带你了解分类是如何实现为类添加方法的. 先说结论,分类中的方法会在编译时变成 category_t 结构体的变量,在运行时合并进主类,分类中的方法会放在主类中方法的前面,主类中原有的方法不会被覆盖.同时,同名的分类方法,后编译的分类方法会“覆盖”先编译的分类方法. 编译时 在编译时,所有我们写的分类,都会转化为 category_t 结

Oracle数据库创建存储过程的示例详解

1.1,Oracle存储过程简介: 存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合,调用存储过程可以简化应用开发人员的很多工作, 减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的. 优点: 允许模块化程序设计,就是说只需要创建一次过程,以后在程序中就可以调用该过程任意次. 允许更快执行,如果某操作需要执行大量SQL语句或重复执行,存储过程比SQL语句执行的要快. 减少网络流量,例如一个需要数百行的SQL代码的操作有一条执行语句完成,不需要在网络中发送数百行代

AngularJS的Filter的示例详解

贴上几个有关Filter使用的几个示例. 1. 首先创建一个表格 <body ng-app="app"> <div class="divAll" ng-controller="tableFilter"> <input type="text" placeholder="输入你要搜索的内容" ng-model="key"> <br><br

bat批处理 if 命令示例详解

if 命令示例详解 if,正如它E文中的意思,就是"如果"的意思,用来进行条件判断.翻译过来的意思就是:如果符合某一条件,便执行后面的命令. 主要用来判断,1.两个"字符串"是否相等:2.两个数值是大于.小于.等于,然后执行相应的命令. 当然还有特殊用法,如结合errorlevel:if errorlevel 1 echo error 或者结合defined(定义的意思):if defined test (echo It is defined) else echo 

Docker-Compose的使用示例详解

Docker Compose是一个用来定义和运行复杂应用的Docker工具.使用Compose,你可以在一个文件中定义一个多容器应用,然后使用一条命令来启动你的应用,完成一切准备工作. - github.com/docker/compose docker-compose是用来在Docker中定义和运行复杂应用的工具,比如在一个yum文件里定义多个容器,只用一行命令就可以让一切就绪并运行. 使用docker compose我们可以在Run的层面解决很多实际问题,如:通过创建compose(基于YU

jQuery.Validate表单验证插件的使用示例详解

jQuery Validate 插件为表单提供了强大的验证功能,让客户端表单验证变得更简单,同时提供了大量的定制选项,满足应用程序各种需求. 请在这里查看示例 validate示例 示例包含 验证错误时,显示红色错误提示 自定义验证规则 引入中文错误提示 重置表单需要执行2句话 源码示例 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <

JavaScript中自带的 reduce()方法使用示例详解

1.方法说明 , Array的reduce()把一个函数作用在这个Array的[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,其效果就是: [x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4) 2. 使用示例 'use strict'; function string2int(s){ if(!s){ alert('the params empty'); return; } if

php示例详解Constructor Prototype Pattern 原型模式

原型模式中主要角色 抽象原型(Prototype)角色:声明一个克隆自己的接口 具体原型(Concrete Prototype)角色:实现一个克隆自己的操作 当一个类大部分都是相同的只有部分是不同的时候,如果需要大量这个类的对象,每次都重复实例化那些相同的部分是开销很大的,而如果clone之前建立对象的那些相同的部分,就可以节约开销. 针对php的一种实现方式就是__construct()和initialize函数分开分别处理这个类的初始化,construct里面放prototype也就是公共的

JavaScript作用域示例详解

作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域示例详解的介绍,希望能帮助大家更好的学习JavaScript. 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 一.JavaScript中无块级作用域 在Java或C#中存在块级作用域

.NetCore实现上传多文件的示例详解

本章和大家分享的是.NetCore的MVC框架上传文件的示例,主要讲的内容有:form方式提交上传,ajax上传,ajax提交+上传进度效果,Task并行处理+ajax提交+上传进度,相信当你读完文章内容后能后好的收获,如果可以不妨点个赞:由于昨天电脑没电了,快要写完的内容没有保存,今天早上提前来公司从头开始重新,断电这情况的确让人很头痛啊,不过为了社区的分享环境,这也是值得的,不多说了来进入今天的正篇环节吧: form方式上传一组图片 先来看看咋们html的代码,这里先简单说下要上传文件必须要

JavaScript中的ajax功能的概念和示例详解

AJAX即"Asynchronous Javascript And XML"(异步JavaScript和XML). 个人理解:ajax就是无刷新提交,然后得到返回内容. 对应的不使用ajax时的传统网页如果需要更新内容(或用php做处理时),必须重载整个网页页面. 示例: html代码如下 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>