前言
前段時間剛到公司,公司在使用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函數中,當值發生改變,就會調用通知的函數。效果如下圖。
代碼地址: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 結構圖
4.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指針。可能為了防止引起內存泄漏問題。
- (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的關系。
- 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/