KVO+FBKVOController使用與原始碼解析
簡介
Key-value observing is a mechanism that allows objects to
be notified of changes to specified properties of other
objects.
簡單來說就是可以通過KVO
監聽物件屬性的變化。
使用
我們簡單的寫一個model
類:Person
如下:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *location;
@end
#import "Person.h"
@implementation Person
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
/**
是否自動控制監聽屬性的變化
@param key 鍵值
@return YES/NO
*/
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if([key isEqualToString:@"name"]){
return NO;
}
return YES;
}
@end
寫一個簡單的測試例子:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self setupViews];
[self setupObservers];
}
- (void)setupViews{
UIButton *changeNameButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 150, 50)];
[changeNameButton setTitle:@"change name" forState:UIControlStateNormal];
changeNameButton.backgroundColor = [UIColor redColor];
changeNameButton.center = CGPointMake(self.view.center.x, 100);
[changeNameButton addTarget:self action:@selector(changeName:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:changeNameButton];
UIButton *changeAgeButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 150, 50)];
[changeAgeButton setTitle:@"change age" forState:UIControlStateNormal];
changeAgeButton.backgroundColor = [UIColor redColor];
changeAgeButton.center = CGPointMake(self.view.center.x, 200);;
[changeAgeButton addTarget:self action:@selector(changeAge:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:changeAgeButton];
}
- (void)setupObservers{
person = [Person new];
person.name = @"xz";
person.age = 20;
person.location = @"深圳";
NSLog(@"before %s",object_getClassName(person));
[person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:nil];
[person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)changeName:(id)sender{
person.name = @"xsc";
NSLog(@"after %s",object_getClassName(person));
}
- (void)changeAge:(id)sender{
person.age = 22;
NSLog(@"after %s",object_getClassName(person));
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
分別點選change name
與change age
輸出的日誌如下:
2017-10-12 16:22:19.606 KVOExample[27496:963593] before Person
2017-10-12 16:22:21.900 KVOExample[27496:963593] {
kind = 1;
new = xsc;
}
2017-10-12 16:22:21.901 KVOExample[27496:963593] after NSKVONotifying_Person
2017-10-12 16:22:23.147 KVOExample[27496:963593] {
kind = 1;
new = 22;
}
2017-10-12 16:22:23.148 KVOExample[27496:963593] after NSKVONotifying_Person
原理分析
Automatic key-value observing is implemented using a
technique called isa-swizzling.
The isa pointer, as the name suggests, points to the
object's class which maintains a dispatch table. This
dispatch table essentially contains pointers to the
methods the class implements, among other data.
When an observer is registered for an attribute of an
object the isa pointer of the observed object is modified,
pointing to an intermediate class rather than at the true
class. As a result the value of the isa pointer does not
necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine
class membership. Instead, you should use the class method
to determine the class of an object instance.
- 1.
isa-swizzling
的實際上是就是物件isa
指標的替換技術。- 2.結合使用中的例子輸出的日誌
after NSKVONotifying_Person
與上述的說明我們不難分析出,當給被觀察的Person
類例項新增觀察者時,預設會觸發生成NSKVONotifying_Person
的子類,子類中重寫了監聽的屬性的set
方法。
To implement manual observer notification, you invoke
willChangeValueForKey: before changing the value, and
didChangeValueForKey: after changing the value. The
example in Listing 3 implements manual notifications for
the balance property
- 1.上述描述瞭如果需要實現手動的觀察者的通知,需要在改變對應的屬性的值前後分別呼叫
willChangeValueForKey:
,didChangeValueForKey:
方法。結合使用中的例子,我們也得出相應的結論:NSKVONotifying_Person
的子類中重寫了Person
屬性的set
方法,方法中分別呼叫了willChangeValueForKey:
,didChangeValueForKey:
以達到通知觀察者的目的。
存在問題與解決
通過使用的例子不難分析出KVO存在如下幾個問題:
- 1.新增觀察者與屬性變化回撥的程式碼邏輯是分開的。
- 2.移除觀察者的操作必須存在,不然會導致記憶體洩漏或Crash。
- 3.屬性變化監聽的回撥只能根據
keyPath
區分寫不同的處理邏輯,程式碼耦合。
因此我們考慮二次封裝KVO
去解決這些問題。我們檢視主流的關於這一塊的封裝facebook
封裝的KVOController其實是一個不錯的選擇。下面我們展開分析。
FBKVOController
FBKVOController的使用
#import "ViewController.h"
#import "Person.h"
#import "FBKVOController.h"
@interface ViewController (){
Person *person;
FBKVOController *KVOController;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
[self setupViews];
[self setupPerson];
// 2.FB對KVO的封裝
[self setupFBKVO];
}
- (void)setupViews{
UIButton *changeNameButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 150, 50)];
[changeNameButton setTitle:@"change name" forState:UIControlStateNormal];
changeNameButton.backgroundColor = [UIColor redColor];
changeNameButton.center = CGPointMake(self.view.center.x, 100);
[changeNameButton addTarget:self action:@selector(changeName:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:changeNameButton];
UIButton *changeAgeButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 150, 50)];
[changeAgeButton setTitle:@"change age" forState:UIControlStateNormal];
changeAgeButton.backgroundColor = [UIColor redColor];
changeAgeButton.center = CGPointMake(self.view.center.x, 200);;
[changeAgeButton addTarget:self action:@selector(changeAge:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:changeAgeButton];
}
- (void)setupPerson{
person = [Person new];
person.name = @"xz";
person.age = 20;
person.location = @"深圳";
NSLog(@"before %s",object_getClassName(person));
}
- (void)setupFBKVO {
KVOController = [FBKVOController controllerWithObserver:self];
[KVOController observe:person
keyPath:@"name"
options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSLog(@"%@",change);
}];
[KVOController observe:person
keyPath:@"age"
options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSLog(@"%@",change);
}];
}
- (void)changeName:(id)sender{
person.name = @"xsc";
NSLog(@"after %s",object_getClassName(person));
}
- (void)changeAge:(id)sender{
person.age = 22;
NSLog(@"after %s",object_getClassName(person));
}
測試結果:
2017-10-12 19:04:32.343 KVOExample[37615:1335210] before Person
2017-10-12 19:04:33.491 KVOExample[37615:1335210] {
FBKVONotificationKeyPathKey = name;
kind = 1;
new = xsc;
}
2017-10-12 19:04:33.492 KVOExample[37615:1335210] after NSKVONotifying_Person
2017-10-12 19:04:35.053 KVOExample[37615:1335210] {
FBKVONotificationKeyPathKey = age;
kind = 1;
new = 22;
}
2017-10-12 19:04:35.054 KVOExample[37615:1335210] after NSKVONotifying_Person
FBKVOController 實現分析
FBKVOController 新增觀察者
+ (instancetype)controllerWithObserver:(nullable id)observer
- (instancetype)initWithObserver:(nullable id)observer
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
- (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;
}
關於NSMapTable
可以檢視NSHashTable & NSMapTable。上述程式碼完成如下工作:
- 1.初始化了一個全域性字典,配置相應的比較策略,用於儲存後續的
KVO
的例項。- 2.初始化一個全域性的鎖,避免多執行緒操作導致資料異常。
FBKVOController 設定觀察的屬性
- (void)observe:(nullable id)object
keyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
block:(FBKVONotificationBlock)block
// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
// observe object with info
[self _observe:object info:info];
- 1.利用傳入的
keyPath
,options
初始化一個_FBKVOInfo
例項,_FBKVOInfo
是一個model
類用來存在KVO
過程中的全部資訊。- 2.觸發真正的新增觀察屬性的操作。
我們深入分析步驟2中的程式碼:
- (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];
}
- 1.每次對全域性
KVO
資訊字典表的操作都需要先執行鎖操作,保證安全性。- 2.以觀察的例項作為鍵值,獲取的集合就是觀察的所有該例項的屬性初始化的
_FBKVOInfo
類的集合。- 3.操作該集合新增新的
_FBKVOInfo
類。
_FBKVOSharedController 真正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];
}
}
- 觀察的例項將
[_FBKVOSharedController sharedController]
例項新增到觀察者中,全域性的上下文傳入初始化好的KVO
的全域性資訊info
,這樣在觸發回撥時可以區分處理。
處理KVO回撥
- (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];
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<NSKeyValueChangeKey, 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];
}
}
}
}
}
- 1.根據回到的context獲取
KVO
的全部資訊,然後選擇block
,'action',原生處理三種不同的方式分發處理。
移除觀察者
回到FBKVOController
類,聚焦到dealloc
函式中,該函式是在物件被釋放時觸發。
- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}
檢視呼叫棧資訊最終的觸發函式如下:
- (void)_unobserveAll
{
// lock
pthread_mutex_lock(&_lock);
NSMapTable *objectInfoMaps = [_objectInfosMap copy];
// clear table and map
[_objectInfosMap removeAllObjects];
// unlock
pthread_mutex_unlock(&_lock);
_FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
for (id object in objectInfoMaps) {
// unobserve each registered object and infos
NSSet *infos = [objectInfoMaps objectForKey:object];
[shareController unobserve:object infos:infos];
}
}
- 1.清理掉全域性儲存的KVO的資訊集合。
- 2.
shareController
中也需要清理儲存的KVO的資訊,同時移除觀察者。參考如下程式碼段:
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
if (0 == infos.count) {
return;
}
// unregister info
pthread_mutex_lock(&_mutex);
for (_FBKVOInfo *info in infos) {
[_infos removeObject:info];
}
pthread_mutex_unlock(&_mutex);
// remove observer
for (_FBKVOInfo *info in infos) {
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
}
參考文章:
相關文章
- OkHttp 開源庫使用與原始碼解析HTTP原始碼
- PyTorch ResNet 使用與原始碼解析PyTorch原始碼
- EventBus原理與原始碼解析原始碼
- GYHttpMock:使用及原始碼解析HTTPMock原始碼
- Optional原始碼解析與實踐原始碼
- Function原始碼解析與實踐Function原始碼
- redis原始碼解析----epoll的使用Redis原始碼
- RxBinding使用和原始碼解析原始碼
- Picasso的使用和原始碼解析原始碼
- EventBus的使用和原始碼解析原始碼
- Dart語法篇之集合的使用與原始碼解析(二)Dart原始碼
- ThreadLocal與ThreadLocalMap原始碼解析thread原始碼
- TextWatcher的使用及原始碼解析原始碼
- DialogFragment使用到原始碼完全解析Fragment原始碼
- [原始碼解析] PyTorch 如何使用GPU原始碼PyTorchGPU
- Webpack原始碼基礎-Tapable從使用Hook到原始碼解析Web原始碼Hook
- 【原始碼解析】AsyncTask的用法與規則原始碼
- 比特幣挖礦與原始碼解析比特幣原始碼
- redux 原始碼解析與實際應用Redux原始碼
- QT Widgets模組原始碼解析與技巧QT原始碼
- Myth原始碼解析系列之七- 訂單下單流程原始碼解析(參與者)原始碼
- 【原始碼解析】- ArrayList原始碼解析,絕對詳細原始碼
- Binder的使用方法和原始碼解析原始碼
- ffmpeg在iOS的使用-iFrameExtractor原始碼解析iOS原始碼
- Retrofit2使用方式和原始碼解析原始碼
- ItemDecoration深入解析與實戰(一)——原始碼分析原始碼
- Tomcat長輪詢原理與原始碼解析Tomcat原始碼
- 【XMPP】Smack原始碼之訊息接收與解析Mac原始碼
- QT Widgets模組原始碼解析與實踐QT原始碼
- Golang的GMP排程模型與原始碼解析Golang模型原始碼
- Spark原始碼-SparkContext原始碼解析Spark原始碼Context
- CountDownLatch原始碼解析CountDownLatch原始碼
- LeakCanary原始碼解析原始碼
- vuex原始碼解析Vue原始碼
- ArrayBlockQueue原始碼解析BloC原始碼
- AsyncTask原始碼解析原始碼
- CopyOnWriteArrayList原始碼解析原始碼
- Express原始碼解析Express原始碼