KVOController 分析

<!doctype html>

KVOController 是由 facebook 开源的 kvo 组件,其特点是简单易用且安全。

KVO现状

kvo 全称 key-value observing,由 cocoa 框架提供的支持观察者模式的技术,结合 Objective-C 非常易用,在很多场合都可以有效地替换 NSNotificationCenter。但其也有一些致命的缺点,导致很容易引发 crash。其使用规则如下:

  • addObserver 和 removeObserver 必须配对出现。

    仅支持以下用法:

    [observee addObserver...];
    //do something
    [observee removeObserver...];
    

    以下用法很大可能会出引起 crash,

    [observee addObserver...];
    //do something
    [observee removeObserver...];
    [observee addObserver...];
    
    [observee addObserver...];
    //do something
    [observee removeObserver...];
    [observee removeObserver...];
    
    [observee removeObserver...];
    
    [observee addObserver...];
    
  • addObserver 和 removeObserver 按照先后次序调用。

    其调用顺序如下所示:

    [observee addObserver...];
    //do something
    [observee removeObserver...];
    

    必须先添加观察者,然后处理业务,最后完成后移除观察者,释放掉观察者。以下用法将导致 Crash:

    [observee removeObserver...];
    //do something
    [observee addObserver...];
    

可以看出,kvo 的使用要求很多,稍有不慎就会导致crash。甚至很多时候明明代码已经做到了配对出现且按次序调用,可是还是发现有相关的 crash,就原因来分,有以下几类:

  • 多线程导致软件运行时环境复杂化。
  • 观察者意外地过早释放。

这些情况都会导致代码的实际运行情况跟我们预期的差别很多,从而产生令人讨厌的却不可避免的 crash。根据以上分析可知,kvo 导致的 crash 有两大类:

  • 多次添加或者移除同一观察者消息,或者在添加之前移除。
  • 发送回调消息到已释放的观察者。

KVOController

面对 cocoa 提供的 kvo 技术存在的各种坑,大家各显神通,提供了各种各样的替代方案,包括作者本人几年前也实现过一个,直到 KVOController 问世。该方案不仅简单易用,且实现也相对简单。

KVOController 优势

实现单例 _FBKVOSharedController 注册和转发 kvo 消息,作为事实的观察者和消息中转站,避免发送消息给已释放的观察者。并且可以做一些检查和过滤操作,避免观察者多次添加和移除针对同一消息的关注,从而避免 crash 问题。KVOController 具有以下特点:

  • 提供更加安全的借口。

    通过解决addObserver 和 removeObserver 必须配对且按次序调用的问题,提供了一套更安全的接口。并在观察者释放时释放掉对应的 KVOController,避免发消息给已释放观察者。

  • 提供更易用的接口。

    提供对 block 回调和自定义 selector 的支持,保持了灵活和强大。

  • 提供线程安全的接口。

KVOController 探究

KVOController 用法

KVOController 使用方式如下(本文采用ARC方式编写代码,如采用MRR请自行管理对象生命期):

FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
_KVOController = KVOController;
//observe clock.date property
[_KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
  clockView.date = change[NSKeyValueChangeNewKey];
}];

这便是 KVOController 的一个完整用法,甚至不需要移除观察者,使用相当方便。_KVOController 为观察者对象的一个实例变量。

KVOController 甚至提供了一种更简便的用法,简化使用方式,如下:

[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];

甚至不需要创建 KVOController,直接使用 KVOController 提供的扩展属性即可。如何实现的呢?大家都猜到了,KVOController 为 NSObject 添加了一个包含 KVOController 方法的 category,采用 lazy creation 方式创建一个 KVOController 对象。其实现如下:

- (FBKVOController *)KVOController {
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey);
  // lazily create the KVOController
  if (nil == controller) {
    controller = [FBKVOController controllerWithObserver:self];
    self.KVOController = controller;
  }

  return controller;
}

KVOController 实现方式

KVOController 消息流

那么问题来了,KVOController是如何做到如此简便的使用方式且同时还保证了安全性?这就要从KVOController的实现分析,其UML类图如下:

从图中可知,其工作流程如下:

  1. observer 调用 FBKVOController 的 observe 方法注册观察者。
  2. FBKVOController 处理观察者信息,并将其封装为 _FBKVOInfo。FBKVOController过滤重复的或者未注册过的观察消息 。_FBKVOInfo 定义如下:
    @interface _FBKVOInfo : NSObject
    @end
    
    @implementation _FBKVOInfo {
     @public
       __weak FBKVOController *_controller;
       NSString *_keyPath;
       NSKeyValueObservingOptions _options;
       SEL _action;
       void *_context;
       FBKVONotificationBlock _block;
    }
    

    可以看出 _FBKVOInfo 定义了跟观察者相关的一些信息,包括 消息、回调函数以及维护对 FBKVOController 的弱引用,_FBKVOInfo 与观察者消息一一对应,即每一个观察者注册信息对应一个 _FBKVOInfo 实例。这样可以做到观察者释放时不再向其发送消息,避免 crash。

  3. FBKVOController 调用中转站 _FBKVOSharedController 的 observe 方法。
  4. _FBKVOSharedController 向 observee 完成真正的观察者注册。
  5. 关注的 keyPath 发生改变时,observee 向 _FBKVOSharedController 发送 kvo 通知。
  6. _FBKVOSharedController 根据 _FBKVOInfo 判断观察者是否存活,若存活,回调 observer。否则吞没该消息。
  7. observer 根据收到的回调完成对应的操作。

observer 与 FBKVOController 为一一对应关系,即一个观察者实例对应一个 FBKVOController 实例,而所有的观察者注册和回调工作都有 _FBKVOSharedController 这个单例完成,其将在软件的整个生命周期内存活。

弱引用

如何避免发送消息给已释放未移除观察者信息的观察者呢?答案就是弱引用!以下代码为 KVOController 的实现片段:

@interface FBKVOController : NSObject
@property (atomic, weak, readonly) id observer;
@end

@implementation FBKVOController
- (void)dealloc {
  [self unobserveAll];
}
@end

从中可明显地看出:

  • KVOController 维护了一个对观察者的弱引用。
  • KVOController 释放时会移除其注册的观察者消息。

结合 _FBKVOInfo 维护的指向 FBKVOController 的弱引用,即使观察者已释放而 KVOController 未释放,由于其维护的是一个指向观察者的弱引用,可以有效地避免发消息给已经释放掉的观察者。由于观察者与 KVOController 一一对应的关系,通常观察者释放时,也会释放掉 KVOController 实例,则会隐式地移除观察者消息。

消息过滤

那么 KVOController 针对多次注册或者移除同一观察者消息的问题又是如何解决的呢?答案就是消息过滤!

如下所示,为 FBKVOController 的 observe 代码片段,通过 _FBKVOInfo *existingInfo = [infos member:info];可知,首先判断是否已经包含对应观察者消息,如果包含则直接返回。

- (void)_observe:(id)object info:(_FBKVOInfo *)info {
  OSSpinLockLock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    NSLog(@"observation info already exists %@", existingInfo);

    OSSpinLockUnlock(&_lock);
    return;
  }

且 FBKVOController 内部采用 NSMapTable 作为容器存储被观察者和对应的观察者信息,NSMapTable 类似于 NSDictionary,但更加灵活和强大,支持添加 NULL 指针、void * 以及 weak 对象等,即允许指定加入容器中项的内存管理方式和类型行为,比如可以指定内存管理方式采用 copy、weak 和 strong 中的一种,还可以设定加入项的相等判断方式,比如指针或者 isEqual: 方法。类似 NSMapTable 的容器还有 NSHashTable 和 NSPointerArray,分别对应于 NSSet 和 NSArray。

FBKVOController 中 NSMapTable的构造方式如下:

NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];

根据传入参数retainObserved可以决定是否对被观察者维持一个强引用,对 NSPointerFunctionsStrongMemory 等的详细解释参考 Pointer Function Options。NSPointerFunctionsStrongMemory 表示对加入的项维持强引用,NSPointerFunctionsWeakMemory 则相反。而 NSPointerFunctionsObjectPointerPersonality 通过指针判断加入的项是否相等,NSPointerFunctionsObjectPersonality 则通过加入对象的 isEqual: 方法判断。其中 key 为被观察者,value 为 _FBKVOInfo 实例,也即观察者消息信息。

KVOController通过提供 _FBKVOSharedController 注册和转发消息,避免观察者直接使用 kvo,通过这个中间层达到了隔离的效果。并且提供一个跟观察者一一对应的 FBKVOController,过滤掉容易出错的注册和移除消息的请求,且 FBKVOController 生命周期跟观察者绑定,则观察者释放时,由 FBKVOController 生成的实例也被释放,从 _FBKVOSharedController 移除对应的观察者信息,避免发消息给已释放观察者导致的crash。

时间: 2024-11-13 08:07:02

KVOController 分析的相关文章

爱奇艺、优酷、腾讯视频竞品分析报告2016(一)

1 背景 1.1 行业背景 1.1.1 移动端网民规模过半,使用时长份额超PC端 2016年1月22日,中国互联网络信息中心 (CNNIC)发布第37次<中国互联网络发展状况统计报告>,报告显示,网民的上网设备正在向手机端集中,手机成为拉动网民规模增长的主要因素.截至2015年12月,我国手机网民规模达6.20亿,有90.1%的网民通过手机上网. 图 1  2013Q1~2015Q3在线视频移动端和PC端有效使用时长份额对比 根据艾瑞网民行为监测系统iUserTracker及mUserTrac

Tomcat启动分析(我们为什么要配置CATALINA_HOME环境变量)

原文:http://www.cnblogs.com/heshan664754022/archive/2013/03/27/2984357.html Tomcat启动分析(我们为什么要配置CATALINA_HOME环境变量) 用文本编辑工具打开用于启动Tomcat的批处理文件startup.bat,仔细阅读.在这个文件中,首先判断CATALINA_HOME环境变量是否为空,如果为空,就将当前目录设为CATALINA_HOME的值.接着判断当前目录下是否存在bin\catalina.bat,如果文件

C# 最佳工具集合: IDE 、分析、自动化工具等

C#是企业中广泛使用的编程语言,特别是那些依赖微软的程序语言.如果您使用C#构建应用程序,则最有可能使用Visual Studio,并且已经寻找了一些扩展来对您的开发进行管理.但是,这个工具列表可能会改变您编写C#代码的方式. C#编程的最佳工具有以下几类: IDE VS扩展 编译器.编辑器和序列化 反编译和代码转换工具 构建自动化和合并工具 版本控制 测试工具和VS扩展 性能分析 APM 部署自动化 容器 使用上面的链接直接跳转到特定工具,或继续阅读以浏览完整列表.

秒杀系统架构分析与实战

0 系列目录 秒杀系统架构 秒杀系统架构分析与实战 1 秒杀业务分析 正常电子商务流程 (1)查询商品:(2)创建订单:(3)扣减库存:(4)更新订单:(5)付款:(6)卖家发货 秒杀业务的特性 (1)低廉价格:(2)大幅推广:(3)瞬时售空:(4)一般是定时上架:(5)时间短.瞬时并发量高: 2 秒杀技术挑战 假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统需要面对的技术挑战有: 对现有网站业务造成冲击 秒杀活动只是网站营销的一个附加活动,

Openfire分析之二:主干程序分析

引言 宇宙大爆炸,于是开始了万物生衍,从一个连人渣都还没有的时代,一步步进化到如今的花花世界. 然而沧海桑田,一百多亿年过去了-. 好复杂,但程序就简单多了,main()函数运行,敲个回车,一行Hello World就出来了,所以没事多敲敲回车,可以练手感-. 一.程序入口 Java的程序入口是main方法,Openfire也不例外.可以全局检索一下"void main",可以看到,Openfire的main函数有两个: (1)org.jivesoftware.openfire.lau

gecode FunctionBranch 源码分析

从名字上看,这个类的核心就在于function, 那么看代码: /// Function to call SharedData<std::function<void(Space& home)>> f; /// Call function just once bool done; 的确是定义了一个function,然后一个状态,猜测是调用了function之后会设置为true,往下看代码: ExecStatus FunctionBranch::commit(Space&

爬虫难点分析

难点分析 1.网站采取反爬策略 2.网站模板定期变动 3.网站url抓取失败 4.网站频繁抓取ip被封 1.网站采取反爬策略 >网站默认对方正常访问的方式是浏览器访问而不是代码访问,为了防止对方使用大规模服务器进行爬虫从而导致自身服务器承受过大的压力,通常网站会采取反爬策略 根据这一特性,我们用代码模拟实现浏览器访问 2.网站模板定期变动-解决方案 >标签变动,比如<div>变动,那么我们不能把代码给写死了 (1)不同配置文件配置不同网站的模板规则 (2)数据库存储不同网站的模板规

R语言学习-词频分析

概念 1.语料库-Corpus 语料库是我们要分析的所有文档的集合,就是需要为哪些文档来做词频 2.中文分词-Chinese Word Segmentation 指的是将一个汉字序列切分成一个一个单独的词语. 3.停用词-Stop Words 数据处理的时候,自动过滤掉某些字或词,包括泛滥的词如Web.网站等,又如语气助词如的.地.得等. 需要加载的包 1.tm包 安装方式:install.packages("tm") 语料库: Corpus(x,readerControl) x-语料

/proc/meminfo分析

参考: 1. linux/Documentation/filesystems/proc.txt 2. Linux 中 /proc/meminfo 的含义 分析文件信息最权威的就是linux自带的文档,Documentation/filesystems/proc.txt较详细地介绍了proc内容. meminfo: Provides information about distribution and utilization of memory.  This varies by architect