前言
前段时间刚到公司,公司在使用FBKVOController,本人一直在使用系统的KVO,没有使用过Facebook的这个框架,使用起来挺方便的,所以安利一波,并且读读源码,本文只是略读,了解了FBKVOController的结构和基本实现,可能他的设计思想还没有深入理解,以后慢慢探讨。
FBKVOController的使用
//
// ViewController.m
// FBKVOControllerDemo
//
// Created by 李林 on 2017/5/17.
// Copyright © 2017年 lee. All rights reserved.
//
#import "ViewController.h"
#import <KVOController/KVOController.h>
@interface ViewController (){
FBKVOController *_kvoCtrl;
}
@property (weak, nonatomic) IBOutlet UIButton *button;
@property (weak, nonatomic) IBOutlet UIView *colorView;
@property (nonatomic, assign) NSInteger index;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.index = 0;
[self.button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
// FBKVOController
_kvoCtrl = [FBKVOController controllerWithObserver:self];
[_kvoCtrl observe:self keyPath:@"index" options:0 action:@selector(changeColor)];
}
- (void)buttonClick {
self.index++;
}
- (void)changeColor {
self.colorView.backgroundColor = [UIColor redColor];
}
@end
使用很简单,监测某个对象的值,然后将selector写入observe函数中,当值发生改变,就会调用通知的函数。效果如下图。
FBKVOController.gif代码地址:https://github.com/lilianmao/FBKVOControllerDemo
源码简析
1. 系统KVO的问题
- 需要手动移除观察者,且移除观察者的时机必须合适;remove时机很重要
- 注册观察者的代码和事件发生处的代码上下文不同,传递上下文是通过 void * 指针;不懂
- 需要覆写 -observeValueForKeyPath:ofObject:change:context: 方法,比较麻烦;覆盖写observeValueForKeyPath方法
- 在复杂的业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观测的对象和属性时,需要在方法中写大量的 if 进行判断;当业务复杂的时候,覆盖写observeValueForKeyPath方法里有大量的if需要判断
2. FBKVOController
在系统KVO存在很多问题的情况下,FBKVOController应运而生,这个KVOController是Facebook的开源框架,github地址。
- 不需要手动移除观察者;框架自动帮我们移除观察者
- 实现 KVO 与事件发生处的代码上下文相同,不需要跨方法传参数;依旧不懂
- 使用 block 来替代方法能够减少使用的复杂度,提升使用 KVO 的体验;block或者selector的方式,方便使用
- 每一个 keyPath 会对应一个属性,不需要在 block 中使用 if 判断 keyPath;一个keyPath对应一个SEL或者block,不需要统一的observeValueForKeyPath方法里写if判断
3. 角色反转
FBKVOController实现了观察者和被观察者的角色反转,系统的KVO是被观察者添加观察者,而FBKVO实现了观察者主动去添加被观察者,实现了角色上的反转,其实就是用的比较方便。
// FBKVOController
_kvoCtrl = [FBKVOController controllerWithObserver:self];
[_kvoCtrl observe:_person keyPath:@"age" options:0 action:@selector(changeColor)];
// 系统的KVO
[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
4. KVOController的实现
4.1 结构图
KVOProgress.png4.2 NSObject分类和KVOController的初始化
给NSObject添加一个FBKVOController分类,用关联对象动态添加属性。
#import <Foundation/Foundation.h>
#import "FBKVOController.h"
@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end
其中有一个是KVOControllerNonRetaining,防止循环引用。实现如下,要用weak指针。下文做试验证明如果一直只用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;
}
4.3 KVOController介绍
在KVOController.h文件,发现三个类:FBKVOInfo、FBKVOSharedController和FBKVOController。
- 其中FBKVOInfo主要是对需要观测的信息的包装,包含了action、block、options等等,改类中重写了hash,isEqual等方法。_FBKVOInfo覆写了
-isEqual:
方法用于对象之间的判等以及方便 NSMapTable 的存储。
@implementation _FBKVOInfo
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
_FBKVOInfoState _state;
}
- FBKVOController是核心类,包含MapTable和pthread_mutex_lock(貌似原来用的是OSSpinLock),其中_objectInfosMap是存储一个对象对应的KVOInfo的映射关系,也就是说这里<id, NSMutableSet<_FBKVOInfo *> *> 中的id就是对象,
MutableSet就是KVOInfos,各种键值观测的包装。
@implementation FBKVOController
{
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
pthread_mutex_t _lock;
}
这张图表现了就是每个被观测者对象和KVOInfo的关系。
objectInfosMap.png- FBKVOSharedController是一个实际操作类,负责将FBKVOController发送过来的信息转发给系统的KVO处理。这里实现了KVO的观测和remove。
@implementation _FBKVOSharedController
{
NSHashTable<_FBKVOInfo *> *_infos;
pthread_mutex_t _mutex;
}
4.4 KVO过程
我们从写的例子开始走起,可以把FBKVOCOntroller的流程看一遍。
(PS:这也是我现在看框架原码的方法)
[_kvoCtrl observe:self keyPath:@"index" options:0 action:@selector(changeColor)];
这个函数- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action
是FBKVOController的,首先是断言判断是否为空,然后创造一个FBKVOInfo,最后调用本身的observe方法,将包装的FBKVOInfo的info传过去。
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action
{
NSAssert(0 != keyPath.length && NULL != action, @"missing required parameters observe:%@ keyPath:%@ action:%@", object, keyPath, NSStringFromSelector(action));
NSAssert([_observer respondsToSelector:action], @"%@ does not respond to %@", _observer, NSStringFromSelector(action));
if (nil == object || 0 == keyPath.length || NULL == action) {
return;
}
// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options action:action];
// observe object with info
[self _observe:object info:info];
}
这个函数- (void)_observe:(id)object info:(_FBKVOInfo *)info
是FBKVOController的,首先是加锁,防止读写干扰。然后我们查找一下这个object对应的MutableSet,如果有对应的KVOInfo的话,那么就不需要再添加入_objectInfosMap中了;如果没有,则创建info,并且加入_objectInfosMap中。最后解锁,将object传给_FBKVOSharedController处理。
- (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];
}
这个- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
函数应该是调用系统的addObserver,添加观察者。其中[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
这句话就是系统的KVO观测,object添加观测者,这里添加的是FBKVOShare,最后KVO的相应函数也在这里,正好呼应。
- (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];
}
}
这里是KVO的相应函数,有木有很熟悉?这里将当初我们传入的一些action或者block执行以下,完美。
- (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];
}
}
}
}
4.4 循环引用的分析
VC1持有_kvoCtrl,_kvoCtrl持有一个_objectInfosMap
,这是一个可以存放弱指针的NSDictionary,这个函数[_objectInfosMap setObject:infos forKey:object];
就是将object和其需要监听的info加入map中。故VC1持有KVOCtrl,KVOCtrl持有map,map持有VC2,也就是VC1持有VC2。这是要如果我们VC2里观测VC1,就会VC2持有VC1。造成循环引用。
致谢
本文主要参考的博客:
https://github.com/facebook/KVOController
https://github.com/Draveness/Analyze/blob/master/contents/KVOController/KVOController.md
http://chaosgsc.com/kvo.html
https://satanwoo.github.io/2016/02/27/FBKVOController/
网友评论