關於KVO的原理,已經有許多文章來敘述,然而說原理的文章不少,但是就是沒有找到一篇KVO實際使用時的。
本文旨在描述了KVO在實際使用中遇到的種種問題。git
KVO是Objective-C對觀察者模式的一種實現,指定一個被觀察對象,當對象的某個屬性發生更改時,觀察者會得到通知的一種機制。 原生的系統api使用大概以下:github
[self.model addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:nil];
複製代碼
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"value"]) {
NSLog(@"new value = %@", change[NSKeyValueChangeNewKey]);
}
}
複製代碼
- (void)dealloc
{
[self.model removeObserver:self forKeyPath:@"value"];
}
複製代碼
然而,在使用原生的代碼的時候,有好幾處不方便的地方:api
不得不說,KVO這套系統提供的API,實在是太不方便,特別是第三條和第四條,每每還會是個偶發的bug,發現時仍是線上bug。bash
YYKit+YYAddForKVO 裏面就提供了一個YYNSObjectKVOBlockTarget的私有類來充當真正的觀察者,每當咱們調用- (void)addObserverBlockForKeyPath:(NSString *)keyPath block:(void (^)(__weak id obj, id oldVal, id newVal))block
時,都把keyPath,block都統統打包到一個字典裏面,而後把YYNSObjectKVOBlockTarget設置成真正的觀察者。
Target收到回調後,在從字典裏面,找到觀察者的block,進行回調。
每當咱們調用removeObserverBlocks時,再獲取到以前存儲的觀察者信息字典,遍歷觀察的key挨個的調用移除。
這套框架很好的解決了上述問題的1,2,3,惟獨對4沒有什麼好的解決辦法框架
KVOController 也採用了和YYAddForKVO相似的方法,把添加觀察者時的keyPath,block都統統打包成一個_FBKVOInfo,而後把_FBKVOSharedController設置成真正的觀察者。
當回調到來時,再取出保存的block回調給外界。
由於FBKVOController經過一個hashMap來保存了本身添加的觀察者信息,因此當屬性dealloc時,關聯對象FBKVOController會先一步dealloc,在這dealloc方法裏面,取出MapTable便可實現自動移除觀察者。post
看起來FBKVOController 很是好的實現了全部KVO中的痛點,然而在其餘場景下,卻帶來了更多的坑
在上述的舉例中,都是A類強持有B類,A類來觀察B類的變化。在這個場景下,YYKit和KVOController表現的都很不錯。咱們把這個場景叫場景0吧。
然而在實際使用中,還存在這2個不一樣的場景:ui
@implementation Model
- (instancetype)init
{
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
// 添加一個定時器來改變屬性
[NSTimer scheduledTimerWithTimeInterval:1.0 block:^(NSTimer * _Nonnull timer) {
weakSelf.value ++;
} repeats:YES];
// 添加觀察者
[self.KVOController observe:self keyPath:@"value" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"new value = %@ %@", change[NSKeyValueChangeNewKey], @(weakSelf.value));
}];
}
return self;
}
- (void)dealloc
{
NSLog(@"dealloc %@", self);
}
@end
複製代碼
觀察到屬性變化時沒有問題,可是,而後釋放掉當前對象就會發現,dealloc沒有走。 這個問題在GitHub上面也有人提出來https://github.com/facebook/KVOController/pull/131。
self對KVOController是強持有的,而KVOController須要實現自動解除觀察者。強持有了observe中存入的參數,本例中也是self,那麼就構成了一個很明顯的循環引用atom
@implementation Model
- (instancetype)init
{
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
[NSTimer scheduledTimerWithTimeInterval:1.0 block:^(NSTimer * _Nonnull timer) {
weakSelf.value ++;
} repeats:YES];
[self.KVOControllerNonRetaining observe:self keyPath:@"value" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"new value = %@ %@", change[NSKeyValueChangeNewKey], @(weakSelf.value));
}];
}
return self;
}
- (void)dealloc
{
[self.KVOControllerNonRetaining unobserveAll];
NSLog(@"dealloc %@", self);
}
@end
複製代碼
這樣使用會有2個問題:spa
unobserveAll方法的源碼:指針
- (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];
}
}
複製代碼
@implementation Model
- (instancetype)init
{
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
[NSTimer scheduledTimerWithTimeInterval:1.0 block:^(NSTimer * _Nonnull timer) {
weakSelf.value ++;
} repeats:YES];
[self addObserverBlockForKeyPath:@"value" block:^(id _Nonnull obj, id _Nullable oldVal, id _Nullable newVal) {
NSLog(@"new value = %@", newVal);
}];
}
return self;
}
- (void)dealloc
{
[self removeObserverBlocks];
NSLog(@"dealloc %@", self);
}
@end
複製代碼
YYKit的框架,雖然沒有自動解除KVO這個特性,可是代碼確實能夠正常work不crash的。
有一個ViewController對象持有了一個model,model經過弱應用引用了ViewController, model須要添加對value的監聽,代碼以下:
@interface ViewController ()
@property (nonatomic, assign) NSInteger value;
@property (nonatomic, strong) Model *model;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
[NSTimer scheduledTimerWithTimeInterval:1.0 block:^(NSTimer * _Nonnull timer) {
weakSelf.value ++;
} repeats:YES];
self.model = [[Model alloc] init];
self.model.vc = self;
[self.model startObserver];
}
複製代碼
@implementation Model
- (void)startObserver
{
[self.KVOController observe:self.vc keyPath:@"value" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"new value = %@", change[NSKeyValueChangeNewKey]);
}];
}
- (void)dealloc
{
NSLog(@"dealloc %@", self);
}
@end
複製代碼
結果依舊是發生了循環引用,self強持有KVOController,KVOController強持有self.vc(ViewController),ViewController強持有了Model,造成了循環引用
@implementation Model
- (void)startObserver
{
[self.KVOControllerNonRetaining observe:self.vc keyPath:@"value" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"new value = %@", change[NSKeyValueChangeNewKey]);
}];
}
- (void)dealloc
{
[self.KVOControllerNonRetaining unobserveAll];
NSLog(@"dealloc %@", self);
}
@end
複製代碼
結果發生了崩潰,緣由同場景1同樣,unobserveAll方法沒能移除任何KVO
總共有三種不一樣的寫法
@implementation Model
- (void)startObserver
{
[self addObserverBlockForKeyPath:@"vc.value" block:^(id _Nonnull obj, id _Nullable oldVal, id _Nullable newVal) {
NSLog(@"new value = %@", newVal);
}];
}
- (void)dealloc
{
[self removeObserverBlocks];
NSLog(@"dealloc %@", self);
}
@end
複製代碼
結果是崩潰。KVO未能及時移除
@implementation Model
- (void)startObserver
{
[self.vc addObserverBlockForKeyPath:@"value" block:^(id _Nonnull obj, id _Nullable oldVal, id _Nullable newVal) {
NSLog(@"new value = %@", newVal);
}];
}
- (void)dealloc
{
[self.vc removeObserverBlocks];
NSLog(@"dealloc %@", self);
}
@end
複製代碼
結果是崩潰。dealloc時,已經獲取不到self.vc了。 KVO未能移除
@implementation Model
- (void)startObserve
{
[self.vc addObserverBlockForKeyPath:@"value" block:^(id _Nonnull obj, id _Nullable oldVal, id _Nullable newVal) {
NSLog(@"new value = %@", newVal);
}];
}
@end
@implementation SecondViewController
- (void)dealloc
{
[self removeObserverBlocks];
NSLog(@"dealloc %@", self);
}
@end
複製代碼
這樣也會崩潰。真正的觀察者,是Model類的關聯對象,dealloc時,關聯對象會先一步釋放,因此仍是會發生KVO未能移除的崩潰
在此場景下,由於Model須要監聽的對象是弱引用的,因此添加了KVO後,難以找到合適釋放的時機。不管哪一個框架,在dealloc方法裏面解除都會發生崩潰
不過也不是說這樣的場景就沒法使用KVO了,筆者仍是研究出了2個能解決的辦法: