1、KVO是基於Runtime機制實現的;
2、當某個類的對象的某個屬性第一次被觀察時,系統會在運行期間動態地創建該類的一個派生類,在這個派生類中重寫基類的任何被觀察屬性的setter方法,派生類在被重寫的setter方法內實現真正的通知機制;
3、如果原類為Person,那麼生成的派生類名為NSKVONotifying_Person;
4、我們知道,每一個對象都有一個isa指針(即第一個成員變量)指向當前類,所以系統就是在當一個類的對象第一次被觀察的時候,偷偷的將isa指針指向動態生成的派生類,從而在 “被監聽屬性” 賦值時執行的是派生類的setter方法;
5、此外,蘋果還偷偷重寫了此派生類的class方法,這時候 po personObj,打印的依舊是Person,但使用po object_getClass(personObj),就可以打印出對象的類型NSKVONotifying_Person;這是蘋果為了隱藏生成的派生類,讓我們誤以為還是使用的當前類;
6、鍵值觀察通知依賴於NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey:;在一個被觀察屬性發生改變之前, willChangeValueForKey: 一定會被調用,這就會記錄舊的值;而當改變發生後,didChangeValueForKey: 會被調用,繼而 observeValueForKey:ofObject:change:context: 也會被調用。
7、注意:willChangeValueForKey:和didChangevlueForKey:缺一不可;
直接拿代碼來說明吧,說了一大堆,估計大家都看的不知所以然。
1、場景:我們聲明了一個Person類,裡面有兩個屬性,name和age,我們要使用kvo來監聽Person對象的age發生的變化;
2、創建Person類,聲明屬性:
#import <Foundation/Foundation.h> /** Person模型類 */ @interface Person : NSObject /** 姓名 */ @property(nonatomic,copy) NSString *name; /** 年齡 */ @property(nonatomic,assign) NSInteger age; @end
3、創建Person對象,添加KVO監聽對象age屬性
#import "ViewController.h" #import "Person.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person *per = [[Person alloc] init]; per.name = @"xiaoming"; per.age = 18; // 觀察per對象的age屬性的變化情況 [per addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL]; // 修改age屬性的值,將會被觀察到 per.age = 20; // 這裡po per的結果:<Person: 0x608000026960> // 但是:po object_getClass(per)的結果:NSKVONotifying_Person // 所以:蘋果偷偷重寫了派生類的class方法,但用運行時可以查看當前對象所屬類型 // 移除KVO [per removeObserver:self forKeyPath:@"age"]; } /** KVO監聽:當被監聽對象的被監聽屬性變化後調用 @param keyPath 被監聽的屬性 @param object 被監聽的對象 @param change 變化情況 @param context 攜帶的參數,此示例傳入NULL即可 */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"%@對象的%@屬性發生變化了%@",object,keyPath,change); // 打印結果: // <Person: 0x608000026960>對象的age屬性發生變化了{ // kind = 1; // new = 20; // } } @end
其實,實現KVO的是這2個方法:willChangeValueForKey:和didChangevlueForKey:
我們只要調用這兩個方法,屬性值不變化,也會被觀察到。
1、給Person類添加一個方法kvoTest:
#import <Foundation/Foundation.h> /** Person模型類 */ @interface Person : NSObject /** 姓名 */ @property(nonatomic,copy) NSString *name; /** 年齡 */ @property(nonatomic,assign) NSInteger age; /** 監聽age屬性發生變化 */ - (void)kvoTest; @end
#import "Person.h" @implementation Person - (void)kvoTest{ [self willChangeValueForKey:@"age"]; [self didChangeValueForKey:@"age"]; } @end
2、調用kvoTest方法監聽對象屬性age的變化:
#import "ViewController.h" #import "Person.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person *per = [[Person alloc] init]; per.name = @"xiaoming"; per.age = 18; // 觀察per對象的age屬性的變化情況 [per addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL]; // kvoTest裡面調用了willChangeValueForKey:和didChangeValueForKey // 所以:不改變age的值,依舊會被監聽到 [per kvoTest]; // 這裡po per的結果:<Person: 0x608000026960> // 但是:po object_getClass(per)的結果:NSKVONotifying_Person // 所以:蘋果偷偷重寫了派生類的class方法,但用運行時可以查看當前對象所屬類型 // 移除KVO [per removeObserver:self forKeyPath:@"age"]; } /** KVO監聽:當被監聽對象的被監聽屬性變化後調用 @param keyPath 被監聽的屬性 @param object 被監聽的對象 @param change 變化情況 @param context 攜帶的參數,此示例傳入NULL即可 */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"%@對象的%@屬性發生變化了%@",object,keyPath,change); // 打印結果: // <Person: 0x608000032ca0>對象的age屬性發生變化了{ // kind = 1; // new = 18; // 因為age的值根本沒有發生變化,所是這裡的new還是18 // } } @end