更友好的KVO

前言

观察者模式是大家在开发过程中每个人都要使用的一种设计模式,在iOS的开发流程中,KVO则是这一开发模式的主要实践手段,观察一个属性,当属性值发生变化的就能能够拿到这个属性的新、老、初始化等行为事件,这种模式对于处理model反馈给Controller层是很实用的,在MVC和MVVM等设计模式中都是有着很广泛的应用。但是苹果提供的KVO的API没有那么的友好,在我们的使用中要写一大堆的代码,还需要在delloc的时候进行移除,这样对于工程代码来说,很明显就是一种“坏味”,问题就出现了,怎么解决这个KVO代码复杂的问题呢?Facebook的工程师给出了他们的方案->KVOController,这个方案很好的解决了上面说的问题,KVO变的非常好用,而且不用自己去移除,只能说太棒了。

KVOController分析

看一下这个库的文件有那些

  • KVOController.h
  • FBKVOController.h
  • NSObject+FBKVOController.h

文件只有三个,一个头文件、一个给NSObject快速生成KVOController文件、最后一个是具体的实现。只能用短小精悍来形容这个库,接着分析一下后两个文件。

NSObject+FBKVOController.h

这个里面运用了runtime里面的AssociatedObject技术,能够快速的生成FBKVOController,这里面分两种,一种是对object本身产生引用的,一种是对object本身不产生引用的,我们看一下代码上的区别:

// 两个不同的地方就是用了不同的初始化方法,在这个文件看来
// 就仅仅如此,具体的不同在另外的文件中去分析
- (FBKVOController *)KVOController
{
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey); // lazily create the KVOController
if (nil == controller) {
controller = [FBKVOController controllerWithObserver:self];
self.KVOController = controller;
} return controller;
} - (FBKVOController *)KVOControllerNonRetaining
{
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey); if (nil == controller) {
// 不同点就是这里,使用不同的初始化方法
controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
self.KVOControllerNonRetaining = controller;
} return controller;
}

这就是这个文件的所有的内容了,另外关于AssociatedObject技术的,这次不做解释

FBKVOController.h

提供了对开发者调用的API,使用这个库,基本上都是这个这个文件打交道,那么看一下为什么这个库这么好用,具体的实现是怎么样的。

那么我们来看一下,为什么用户使用FBKVOController来处理添加了一个观察的路径,这个路径就能和系统的一样给我们提供观察回调呢?以最常用的

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;

为入口,来进行分析。

- (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的东西,然后又调用[self _observe:object info:info];这个方法,就能完成功能了。先看一下_FBKVOInfo:

- (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;
}

这个里面记录了观察者、路径、时间处理的block和action以及NSKeyValueObservingOptions和一个context,主要就是信息记录的一个类,需要注意的是_controller是一个弱引用,不会产生循环引用,这个类也就是这个功能,很简单。

接着看一下[self _observe:object info:info]这个方法

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
OSSpinLockLock(&_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
OSSpinLockUnlock(&_lock);
return;
} // lazilly create set of infos
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
} // add info and oberver
[infos addObject:info]; // unlock prior to callout
OSSpinLockUnlock(&_lock); [[_FBKVOSharedController sharedController] observe:object info:info];
}

首先看到的是给一个NSMapTable加锁,保证数据读写安全,然后去用object作为key去获取一个_FBKVOInfo的集合,然后判断这个_FBKVOInfo是否已经集合中存在,此处需要注意的是_FBKVOInfo重写了equial方法,如果不是,就把这个_FBKVOInfo添加到集合中去,然后开锁,这里要提到,此处为什么要使用NSMapTable,一个重要的愿意是用NSMapTable能够以弱引用的方式存储值,这一点在常见的字典和数值中都做不到的(http://www.tuicool.com/articles/NRJNJjr)。最后,调用

[[_FBKVOSharedController sharedController] observe:object info:info];继续去完成观察的绑定。那接下来就看这个方法:


- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
} // register info
OSSpinLockLock(&_lock);
[_infos addObject:info];
OSSpinLockUnlock(&_lock); // 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];
}
}

这里吧info添加到_infos这个NSHashTable(key是weak的)对象中去,然后执行真长的添加观察者的操作,可以看到,在这个FBKVO里面所有的观察的都是在这个_FBKVOSharedController单例里面完成的,如果info里面是不观察,就移除掉这个keypath,另外context:(void *)info这点需要注意,直接关系到回调的执行。最后就是执行的代码了:

- (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
OSSpinLockLock(&_lock);
info = [_infos member:(__bridge id)context];
OSSpinLockUnlock(&_lock);
} 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) {
info->_block(observer, object, change);
} 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];
}
}
}
}
}

很简单,就是拿到info的信息,根据里面记录的变量的值来进行处理回调等事件,到这里一个完整的观察流程就完成了,要说明的是,这个单例会存在在内存中,KVOController会管理自己管理添加进去的key,在释放的会unobserveAll,也就是把自己添加到单例里面的全部取出来。至于如何去除观察,原理是同样的。

总结一下

这个库代码很简单,里面很能体现作者在内存方面的考虑,防止内存引用循环,另外,当初里面会有个单例在里面是我没有想到的,这个单例能让所有的观察都在同一地方,对代码的控制是相当的有功力,我相信简化KVO的思路大家都有过,但是真的把这个东西能完美的做出来,还是需要花费挺大的心思。真的很感谢这些开源代码的贡献者,由衷的敬佩。

友好的KVO的更多相关文章

  1. iOS---观察者模式之--->KVO

    文章结构如下: Why? (为什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) More (更多细节) 原理 自己实现KVO 在我的上一篇文章浅谈 iOS Notifica ...

  2. 面向未来的友好设计:Future Friendly

    一年前翻译了本文的一部分,最近终于翻译完成.虽然此设计思想的提出已经好几年了,但是还是觉得应该在国内推广一下,让大家知道“内容策略”,“移动优先”,“响应式设计”,“原子设计”等设计思想和技术的根源. ...

  3. salesforce 零基础学习(五十四)常见异常友好消息提示

    异常或者error code汇总:https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_con ...

  4. Objective-C之KVC、KVO

    1,KVC(键值编码)  Key Value Coding 1.1在C#中,可以通过字符串反射来获取对象,从而对对象的属性进行读写,Object-C中有同样的实现,通过字符串(属性名词)对对象的属性进 ...

  5. OS 如何选择delegate、notification、KVO?

    原文链接:http://blog.csdn.net/dqjyong/article/details/7685933 前面分别讲了delegate.notification和KVO的实现原理,以及实际使 ...

  6. KVC 和 KVO

    KVC 键值编码    全称是Key-value coding,翻译成键值编码.它提供了一种使用字符串而不是访问器方法去访问一个对象实例变量的机制.        1.通过key(成员变量的名称)设置 ...

  7. 11. KVC And KVO

    1. KVC And KVO  的认识 KVC/KVO是观察者模式的一种实现  KVC全称是Key-value coding,翻译成键值编码.顾名思义,在某种程度上跟map的关系匪浅.它提供了一种使用 ...

  8. KVO __ 浅谈

    KVO :Key-Value Observing 它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知.简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了. ...

  9. iOS开发系列--Objective-C之KVC、KVO

    概述 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby.Python的动态特性,例如动态类型.动态加载.动态绑定等.今天我们着重介绍ObjC中的键值编码(KVC).键值监听( ...

随机推荐

  1. perl6: hash小笔记

    > ,,, { => , => } > my $a = :%h h => { => , => } > $a.perl :h({, }) > my ...

  2. 用C代码简要模拟实现一下RPC(远程过程调用)并谈谈它在代码调测中的重要应用【转】

    转自:http://blog.csdn.net/stpeace/article/details/44947925 版权声明:本文为博主原创文章,转载时请务必注明本文地址, 禁止用于任何商业用途, 否则 ...

  3. Linux Module框架【转】

    转自:http://www.cnblogs.com/LittleHann/p/4558719.html catalog 1. 概述 2. 使用模块 3. 插入和删除模块 4. 自动化与热插拔 5. 版 ...

  4. javascript多投事件的处理 (转)

    出处 http://blog.csdn.net/dead_of_winter/article/details/1646367 尽管ecma标准指定了addEventListener这样的方法来实现事件 ...

  5. html- 头部元素

    一:HTML <head> 元素 <head> 元素是所有头部元素的容器.<head> 内的元素可包含脚本,指示浏览器在何处可以找到样式表,提供元信息,等等. 以下 ...

  6. 数据库--mysql介绍

    一:什么是数据库 数据库(Database)是按照数据结构来组织.存储和管理数据的仓库, 每个数据库都有一个或多个不同的API用于创建,访问,管理,搜索和复制所保存的数据. 我们也可以将数据存储在文件 ...

  7. GreenPlum学习笔记:split_part与string_to_array字符截取

    偶遇一个需求:想按某个指定符号分割之后,提取字符. 例如:tag = '休闲,娱乐,运动,玩耍',想提取"休闲"这个词. 方法一:string_to_array select st ...

  8. c++实现二叉树的非递归创建以及非递归先序、中序、后序遍历

    二叉树的创建 思路:数组中从上到下依次放着二叉树中的元素,使用递归很容易实现,那么这里使用容器来存放之前的状态实现循环创建二叉树. TreeNode* createTree(int *arr, int ...

  9. 安装VM虚拟机提示 尝试创建目录 C:\Public\documents\SharedVirtual Machines 时发生错误解决方法

    把Windows Defender安全中心的“受控制文件夹的访问”给关闭了,然后就可以顺利安装上了. 作者:耑新新,发布于  博客园 转载请注明出处,欢迎邮件交流:zhuanxinxin@foxmai ...

  10. day7 socket网络编程基础

    Socket Socket是什么? 下面来看一下网络的传输过程: 上面图片显示了网络传输的基本过程,传输是通过底层实现的,有很多底层,我们写传输过程的时候,要知道所有的过程那就太复杂了,socket为 ...