前言:KVC和KVO是幫助我們駕馭objective C動態特性工具。KVO是建立在KVC基礎上的,所以不了解KVC的同學可以參見我的這篇博客。這裡我不會再重復講解KVC。
本文的內容
KVO的定義
KVO的典型使用場景。
手動KVO
幾點KVO要說的地方
KVO提供了一種key-value-observing的機制,也就是說可以通過監聽key,來獲得value的變化。用來在對象之間監聽狀態變化。使用KVO的類要遵循 協議,事實上,任何繼承自NSObject的類,都遵循了這個協議。而Object C中,幾乎所有的類都源自NSObject
使用KVO通常分為三步
用函數
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
注冊通知
observer:觀察者,也就是KVO通知的訂閱者。訂閱著必須實現
observeValueForKeyPath:ofObject:change:context:方法 keyPath:描述將要觀察的屬性,相對於被觀察者。 options:KVO的一些屬性配置;有四個選項。 context: 上下文,這個會傳遞到訂閱著的函數中,用來區分消息,所以應當是不同的。
options所包括的內容
NSKeyValueObservingOptionNew:change字典包括改變後的值 NSKeyValueObservingOptionOld:change字典包括改變前的值 NSKeyValueObservingOptionInitial:注冊後立刻觸發KVO通知 NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次)
每當監聽的keyPath發生變化了,就會在這個函數中回調。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
keyPath:被監聽的keyPath , 用來區分不同的KVO監聽。 object: 被觀察修改後的對象(可以通過object獲得修改後的值) change:保存信息改變的字典(可能有舊的值,新的值等) context:上下文,用來區分不同的KVO監聽。
通常使用兩個函數
- (void)removeObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context
一般在remove的時候會這麼寫,因為remove的時候,無法判讀啊remove是否成功了
@try {
[object removeObserver:self forKeyPath:keyPath];
}
@catch (NSException *exception) {
NSLog(@%@,exception);
}
這裡,你將看到一個完整的KVO的例子。和上篇KVC一樣,我寫了個類似的demo。點擊random會隨機改變User的age,然後UI上要進行同步顯示出新的和舊的age。
實現過程如下:
定義了一個User類來作為Model
@interface User : NSObject
@property (strong,nonatomic) NSString * name;
@property (nonatomic) NSUInteger age;
@end
定義兩個靜態的變量,一個作為keyPath,一個作為context
static NSString * observename = @age;
static void * privateContext = 0;
然後在viewWillAppear中注冊(訂閱)KVO,在viewWillDisappear中刪除KVO
-(void)viewWillAppear:(BOOL)animated{
[self.user addObserver:self
forKeyPath:observename
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:privateContext];
}
-(void)viewWillDisappear:(BOOL)animated{
@try {
[self.user removeObserver:self forKeyPath:observename];
}
@catch (NSException *exception) {
NSLog(@%@,exception);
}
}
當點擊random的時候,age會改變
- (IBAction)random:(id)sender {
self.user.age = arc4random()%100 +1;
}
然後,在上面提到的函數中進行model和view的同步
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (context == privateContext) {
if ([keyPath isEqualToString:observename]) {
NSNumber * old = [change objectForKey:NSKeyValueChangeOldKey];
NSNumber * new = [change objectForKey:NSKeyValueChangeNewKey];
self.lastvalue.text = [NSString stringWithFormat:@%@,old];
self.newvalue.text = [NSString stringWithFormat:@%@,new];
}
}
}
KVO的實現,是對注冊的keyPath中自動實現了兩個函數,在Setter中,自動調用。
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
可能有時候,我們要實現手動的KVO
這時候需要關閉自動生成KVO通知,然後手動的調用,手動通知的好處就是,可以靈活加上自己想要的判斷條件。例如
+(BOOL)automaticallyNotifiesObserversOfAge{
return NO;
}
-(void)setAge:(NSUInteger)age{
if (age < 22) {
return;
}
[self willChangeValueForKey:@age];
_age = age;
[self didChangeValueForKey:@age];
}
由於Context通常用來區分不同的KVO,所以context的唯一性很重要。通常,我的使用方式是通過在當前.m文件裡用靜態變量定義。
static void * privateContext = 0;
KVO的響應和KVO觀察的值變化是在一個線程上的,所以,大多數時候,不要把KVO與多線程混合起來。除非能夠保證所有的觀察者都能線程安全的處理KVO
改變前和改變後分別為
id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];