KVO 全稱是Key Value Observing,翻譯成鍵值觀察。提供了一種當其它對象屬性被修改的時候能通知當前對象的機制。
KVO 的基本使用:
(1)註冊指定Key路徑的監聽器:
/** 參數
* addObserver: 監聽對象
* forKeyPath: 監聽屬性Key
* options: 監聽可選項
* NSKeyValueObservingOptionNew: 監聽改變後的新值
* NSKeyValueObservingOptionOld: 監聽改變後的舊值
* context: 傳入的上下文
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
(2)刪除指定Key路徑的監聽器:
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
(3)回調監聽:
- (void)observeValueForKeyPath:(NSString *)keyPath //監聽的屬性值
ofObject:(id)object //監聽的對象
change:(NSDictionary<NSString *,id> *)change //值的改變(由options參數決定傳入新值或者舊值)
context:(void *)context //傳入的上下文內容
值得注意的是:不要忘記解除註冊,否則會導致資源泄露。
設置屬性
將觀察者與被觀察者註冊好之後,就可以對觀察者對象的屬性進行操作,這些變更操作就會被通知給觀察者對象。注意,只有遵循 KVO 方式來設置屬性,觀察者對象纔會獲取通知,也就是說遵循使用屬性的 setter 方法,或通過 key-path 來設置:
target.age = 30;
[target setAge:30];
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
下面看一個小 Demo:
我們設想一個場景,當自己住酒店的時候,當酒店給我換房間的時候,我們要得到提醒,才能找對自己的房間。我們依次爲例:
//ViewController
#import "ViewController.h"
#import "Person.h"
#import "Room.h"
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) Room *room;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.room = [[Room alloc] init];
//設置房間的號碼
self.room.no = 10;
//Person 監聽 Room 編號的變化
[self.room addObserver:self.person forKeyPath:@"no" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//房間號變爲20
self.room.no = 20;
}
@end
//Person.m 文件
#import "Person.h"
@implementation Person
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"no"]) {
NSLog(@"Person 檢測到 Room 的屬性: %@ 值改變: %@", keyPath, change);
}
}
//移除觀察者對象,防止內存泄漏
- (void)dealloc{
[self.room removeObserver:self forKeyPath:@"no"];
}
@end
運行,我們得到:
KVO 的內部實現
下面我們分析下,KVO 的內部實現:
1> KVO 是基於 runtime 的 isa-swizzing 機制實現的;
2> 當類 A 的對象第一次被觀察的時候,系統會在運行期動態創建類A 的派生類。系統命名爲NSKVONotifying_A。
3> 在派生類 NSKVONotifying_A 中重寫類 A 的setter方法,NSKVONotifying_A類在被重寫的setter方法中實現通知機制。
4> 其中鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey: 在一個被觀察屬性發生改變之前, willChangeValueForKey: 一定會被調用,這就 會記錄舊的值。而當改變發生後,observeValueForKey:ofObject:change:context: 會被調用,繼而 didChangeValueForKey: 也會被調用。如果可以手動實現這些調用,就可以實現“手動觸發”了(後面介紹)。
5> 類 NSKVONotifying_A會重寫 class方法,將自己僞裝成類A。類 NSKVONotifying_A 還會重寫 deallo 方法來釋放資源。
系統將所有指向類 A 對象的isa指針指向類 NSKVONotifying_A 的對象。
爲了證明上述過程:我們第一步註釋掉ViewController 添加觀察者的代碼,在運行的時候,查看類 Room 的 isa 指針的值:
當將添加觀察者處的代碼打開,我們觀察到,在運行的時候,Room 的 isa指針指向了NSKVONotifying_Room 類(派生類)
KVO 手動實現
在 Room.m 文件中實現:
/**
首先,需要手動實現屬性的 setter 方法,並在設置操作的前後分別調用 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用於通知系統該 key 的屬性值即將和已經變更了;
其次,要實現類方法 automaticallyNotifiesObserversForKey,並在其中設置對該 key 不自動發送通知(返回 NO 即可)。這裏要注意,對其它非手動實現的 key,要轉交給 super 來處理。
*/
#import "Room.h"
@implementation Room
- (void)setNo:(int)no{
[self willChangeValueForKey:@"no"];
_no = no;
[self didChangeValueForKey:@"no"];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"no"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
當我們再次運行觀察,發現 Room 的 isa 指針指向Room類:
參考:
http://www.cppblog.com/kesalin/archive/2012/11/17/kvo.html
申明
以上觀點,屬於個人的理解,如果錯誤之處,歡迎拍磚。