今天在調(diào)用自己一個工具類的時候遇到了這個問題,大致的意思是注冊了觀察者,然后沒有被注銷掉,又開始重復使用了。也可以說 KVO 使用不當造成的,所以在此先來了解下 KVO。
通俗的說,KVO 就是我們是用來設值或取值的協(xié)議(NSKeyValueCoding
)。通過對變量和函數(shù)名進行規(guī)范達到方便設置類成員值的目的。它是Cocoa的一個重要機制,它有點類似于Notification,但是,它提供了觀察某一屬性變化的方法,而Notification需要一個發(fā)送notification的對象,這樣KVO就比Notification極大的簡化了代碼。
KVO 如何工作的呢?
- 1、兩個對象,其中一個對象的屬性發(fā)生改變的時候,另一個對象可以監(jiān)測到。
// 對象(被觀察者)
@interface Student : NSObject
// 觀察者
@interface StudentObserver : NSObject
- 2、兩個對象間通過 ”addObserver:forKeyPath:options:context:“建立起連接
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
- 3、為了能夠響應消息,作為觀察者的對象必須實現(xiàn)下面這個方法。這個方法實現(xiàn)如何響應變化的消息。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
- 4、假如遵循KVO規(guī)則的話,當被觀察的對象的屬性改變的時候,就會直接調(diào)用上面那個方法啦,同時移除掉對觀察者的監(jiān)聽。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
上述案例的完整代碼
#import <Foundation/Foundation.h>
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end
#import "StudentObserver.h"
@implementation StudentObserver
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
NSLog(@"old = %@",[change objectForKey:NSKeyValueChangeOldKey]);
NSLog(@"new = %@",[change objectForKey:NSKeyValueChangeNewKey]);
NSLog(@"context:%@",context);
}
@end
Student *student = [[Student alloc] init];
student.name = @"yang";
StudentObserver *studentObserver = [[StudentObserver alloc] init];
[student addObserver:studentObserver
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
context:(void*)self];
student.name = @"liu";
[student removeObserver:studentObserver forKeyPath:@"name"];
/**
* input
old = yang
new = liu
context:<ViewController: 0x7f98c84a0720>
*/
通過上面闡述的,我們大致了解了 KVO可以很好的觀察某一屬性變化的方法。
詳細了解下下面幾個方法
注冊觀察者方法
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
* anObserver:觀察者對象,這個對象必須實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法,以響應屬性的修改通知。
* keyPath:被監(jiān)聽的屬性。這個值不能為nil。
* options:監(jiān)聽選項,這個值可以是NSKeyValueObservingOptions選項的組合。關(guān)于監(jiān)聽選項,我們會在下面介紹。
* context:任意的額外數(shù)據(jù),我們可以將這些數(shù)據(jù)作為上下文數(shù)據(jù),它會傳遞給觀察者對象的observeValueForKeyPath:ofObject:change:context:方法。這個參數(shù)的意義在于用于區(qū)分同一對象監(jiān)聽同一屬性(從屬于同一對象)的多個不同的監(jiān)聽
// options
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
// 提供屬性的新值
NSKeyValueObservingOptionNew = 0x01,
// 提供屬性的舊值
NSKeyValueObservingOptionOld = 0x02,
// 如果指定,則在添加觀察者的時候立即發(fā)送一個通知給觀察者,
// 并且是在注冊觀察者方法返回之前
NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,
// 如果指定,則在每次修改屬性時,會在修改通知被發(fā)送之前預先發(fā)送一條通知給觀察者
// 這與-willChangeValueForKey:被觸發(fā)的時間是相對應的。
// 這樣,在每次修改屬性時,實際上是會發(fā)送兩條通知。
NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08
};
處理屬性觀察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
keyPath:即被觀察的屬性,與參數(shù)object相關(guān)。
object:keyPath所屬的對象。
change:這是一個字典,它包含了屬性被修改的一些信息。這個字典中包含的值會根據(jù)我們在添加觀察者時設置的options參數(shù)的不同而有所不同。
context:這個值即是添加觀察者時提供的上下文信息。
// change key
FOUNDATION_EXPORT NSString *const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSString *const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSString *const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSString *const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSString *const NSKeyValueChangeNotificationIsPriorKey NS_AVAILABLE(10_5, 2_0);
再通過一個我們可能用到的例子加深理解,監(jiān)聽tableView的 contentOffset 中的偏移量。
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView addObserver: self
forKeyPath: @"contentOffset"
options: NSKeyValueObservingOptionNew
context: nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
CGFloat offset = self.tableView.contentOffset.y;
NSLog(@"self.table.contentOffset === %f",offset);
}
- (void)dealloc {
[self.tableView removeObserver:self
forKeyPath:@"contentOffset"
context:nil];
}
像dealloc 中最好是加一個 @try異常捕獲的寫法,更安全。
- (void)dealloc {
@try {
[self.item removeObserver:self forKeyPath:@"content"];
}
@catch (NSException *exception) {
NSLog(@"Exception: %@", exception);
}
@finally {
// Added to show finally works as well
}
}
總得說來,KVO 很強大,也有很多坑,更詳細的了解可以仔細看看Foundation: NSKeyValueObserving(KVO),至此筆記先到這。
回過頭了,上述那個問題,還是沒有說怎么解決,首先要說明的是使用 KVO 需要小心,需要養(yǎng)成好習慣。上述那問題就是提示我們注冊了觀察者不要忘記了注銷,可以嘗試在 dealloc 中注銷下試試。
備注
http://zhangbuhuai.com/2015/04/29/understanding-KVO/
http://southpeak.github.io/blog/2015/04/23/cocoa-foundation-nskeyvalueobserving/