在开发中,相信不到朋友都会用到kvo来监听对象的属性值,其低耦合的特性在很多场合都可以排上用场。首先说一下使用系统提供的KVO接口来完成kvo的缺点
-
步骤相对比较 :
a.添加监听
b.实现监听回调
c.在适当的地方移除监听 -
如果一个对象同事监听多个属性值,或需要监听不同的对象属性这需要在同一个回调方法中用if - else 来判别是来自哪个对象的哪个属性改变时的回调。这使的回调方法的代码相对比较臃肿,不利于后期阅读维护。
-
最重要的一点 , 必须在正确的地方移除监听。不然很容易会出现野指针错误。
FBKVOViewController是facebook团队开源的框架,提供简介的接口帮我调用kvo的功能。其核心的设计是 :利用单例对象(_FBKVOSharedController)来接受对象属性改变的回调。保证了回调的安全(解决了上面说的缺点3)。当接受到观察对象的属性改变回调时 ,在根据context,向上转发回调到你定义的代码块中。

我们看下FBKVOViewControllerd的简单使用
// create KVO controller instance
//strong reference _KVOController
_KVOController = [FBKVOController controllerWithObserver:self];
// handle clock change, including initial value
[_KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
// update observer with new value
CLOCK_LAYER(clockView).date = change[NSKeyValueChangeNewKey];
}];
就是这样几句简单的代码就已经完成了kvo的所有步骤,简介明了,用block来接收回调,在也不用写if - else 这要的区分逻辑的代码,完全符合了 代码设计的 高内聚 低耦合 的理念。
接下来我们分析一下框架源码的实现
框架中用到了一个比较特殊的键值对存储容器NSMapTable
,这个类与 NSDictority
最大区别在于它对键、值的引用关系和相等性的判断可以自定义
NSDictority
的键值对引用关系
[key (copy) : object (strong)]
NSMapTable
的键值对引用关系
[key (copy,strong , weak...) : object (copy,strong , weak...)]
如果使用的是weak,当key、value在被释放的时候,会自动从NSMapTable中移除对应的项。在
NSMapTable
初始化中可以对键值对的持有和对比策略进行初始化,我们来看下这个NSPointerFunctionsOptions
位移枚举值代表的意思
NSPointerFunctionsStrongMemory
创建了一个retain/release对象的集合,非常像常规的NSSet或NSArray。NSPointerFunctionsWeakMemory
使用等价的__weak来存储对象并自动移除被销毁的对象。NSPointerFunctionsCopyIn
在对象被加入到集合前拷贝它们。
NSPointerFunctionsObjectPersonality使用对象的hash和isEqual 来确定相等性。NSPointerFunctionsObjectPointerPersonality
使用哈希值的移位指针和直接比较指针来确定相等性
框架中考虑到用对象作为key , copy对象的话性能消耗比较大,因此采用了NSMapTable
容器的方案
FVKVOController
属性介绍
//上层观察者,最终处理回调的对象
@property (nullable, nonatomic, weak, readonly) id observer;
//存放被观察对象及属性的数据 (类似字典, 到时其存储的键值关系为 [object : NSMutableSet<_FBKVOInfo *> *>]
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
//线程锁 ,解决多线程,资源竞争问题
pthread_mutex_t _lock;
方法介绍
初始化方法
// 参数retainObserved : 是否强引用被观察的对象。默认是 YES 强引用被观察者 , 但是 ,如果你的被观察者对象对观察者对象有强引用关系的话,必须要 NO 不然会引起 retain cycle ,从而导致内存泄漏
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
//这里是弱引用
_observer = observer;
// keypotions 用了 强引用 + 指针比较取值的策略来管理这个map容器
// valueOptions用了 强引用 + 指针比较
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
pthread_mutex_init(&_lock, NULL);
}
return self;
}
添加观察者方法 - (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
主要是 创建一个 FBKVOInfo
类来记录要用到的信息 ,我们来看下这个的成员变量
__weak FBKVOController *_controller; //对应的KVOController
NSString *_keyPath; //要监听的keypath
NSKeyValueObservingOptions _options;//监听的option
SEL _action; //最终属性改变的回调方法
void *_context;//你自己定义回调时传进的context
FBKVONotificationBlock _block; //最终属性改变的代码块
_FBKVOInfoState _state;//监听信息的状态
然后把这个info传进来下面这个方法
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);
//重map中取出object 对应监听的kepath容器
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// 检测是否已经监听object 的kepath
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) { //如果监听过就直接返回了 ,避免重复监听
pthread_mutex_unlock(&_lock); //解锁
return;
}
// lazilly create set of infos
if (nil == infos) { //这里是从来没有监听过object的任何对象
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
//把对应kepath的info加入到 object对应的 infos容器(NSMutableSet)中
// add info and oberve
[infos addObject:info];
// unlock prior to callout
pthread_mutex_unlock(&_lock);
//请看下面对该类的介绍
[[_FBKVOSharedController sharedController] observe:object info:info];
}
_FBKVOSharedController这个就是最终执行addobserve的类。我们先看下它的的两个成员变量
NSHashTable<_FBKVOInfo *> *_infos; //存放所用使用框架添加kvoInfo
pthread_mutex_t _mutex; //线程所锁
NSHashTable
与NSSet
的区别 跟NSMapTable
与NSDictority
的区别相似,就是对对象的持有策略和比较对象的相等性可以自定义。
添加监听的方法实现 , 很简单,就是判断上面传过来的info
为非空时 就把其加入到 _infos
容器中记录下来,并调用系统添加kvo的方法吧自己作为监听回调者来处理属性改变的回调,并设置info的状态 。最后 把info作为context传给监听方法
- (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) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
收到被观察者属性改变是的回调实现 : 完成转发回你的自己想收到监听的对象
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, 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]; //判断是否是曾经添加的有效的info (未被移除监听的info)
pthread_mutex_unlock(&_mutex);
}
if (nil != info) { //有效的info
// 这个就是我们最初强引用的 FBKVOController , 如果你要最终要收到监听的对象(持有 FBKVOController的对象)销毁了 , 这里就会直接过滤调不处理了,这也是框架安全转发kvo处理的原因。
FBKVOController *controller = info->_controller;
if (nil != controller) {
// 真正处理kvo回调的对象。weak
id observer = controller.observer;
if (nil != observer) {
//转发监听处理
if (info->_block) { //执行你自己传进的block
NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
//重组键值对数据 加入@{ FBKVONotificationKeyPathKey : keyPath }
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) { //执行你自己定义的监听回调sel
#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 的系统回调方法
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
移除监听实现
简单看下移除监听的实现,在FBKVOController
的 infos
中移除掉对应的[ objctkey(被观察者)
:value(infos)
], 或者移除移除infos中的某个对应项(单独移除某个kepath),最终会调用到_FBKVOSharedController
中 让其把 _infos里的每个info移除,并移除系统的监听关系[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
_FBKVOInfo 重写了 isEqual
与 hash
方法是为了 给NSHashTable
的判别相等性提供依据。
关于 hash
的重写意义可以看下这边文章的介绍
网友评论