iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?) 如何手動觸發KVObash
首先須要瞭解KVO基本使用,KVO的全稱 Key-Value Observing
,俗稱「鍵值監聽」,能夠用於監聽某個對象屬性值的改變。app
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
p1.age = 1;
p1.age = 2;
p2.age = 2;
// self 監聽 p1的 age屬性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@改變了%@", object, keyPath,change);
}
// 打印內容
監聽到<Person: 0x604000205460>的age改變了{
kind = 1;
new = 10;
old = 2;
}
複製代碼
上述代碼中能夠看出,在添加監聽以後,age屬性的值在發生改變時,就會通知到監聽者,執行監聽者的observeValueForKeyPath
方法。框架
經過上述代碼咱們發現,一旦age
屬性的值發生改變時,就會通知到監聽者,而且咱們知道賦值操做都是調用set
方法,咱們能夠來到Person
類中重寫age
的set
方法,觀察是不是KVO
在set
方法內部作了一些操做來通知監聽者。函數
咱們發現即便重寫了set
方法,p1對象和p2對象調用一樣的set方法,可是咱們發現p1除了調用set方法以外還會另外執行監聽器的observeValueForKeyPath
方法。ui
說明KVO
在運行時獲取對p1對象作了一些改變。至關於在程序運行過程當中,對p1對象作了一些變化,使得p1對象在調用setAge:
方法的時候可能作了一些額外的操做,因此問題出在對象身上,兩個對象在內存中確定不同,兩個對象可能本質上並不同。接下來來探索KVO內部是怎麼實現的。spa
首先咱們對上述代碼中添加監聽的地方打斷點,看觀察一下,addObserver方法對p1對象作了什麼處理?也就是說p1對象在通過addObserver方法以後發生了什麼改變,咱們經過打印isa指針以下圖所示命令行
經過上圖咱們發現,p1
對象執行過addObserver
操做以後,p1
對象的isa
指針由以前的指向類對象Person
變爲指向NSKVONotifyin_Person
類對象,而p2對象沒有任何改變。也就是說一旦p1對象添加了KVO
監聽之後,其isa
指針就會發生變化,所以set
方法的執行效果就不同了。指針
那麼咱們先來觀察p2
對象在內容中是如何存儲的,而後對比p2
來觀察p1
。 首先咱們知道,p2
在調用setAge:
方法的時候,首先會經過p2
對象中的isa
指針找到Person
類對象,而後在類對象中找到setAge:
方法。而後找到方法對應的實現。以下圖所示code
可是剛纔咱們發現p1
對象的isa
指針在通過KVO
監聽以後已經指向了NSKVONotifyin_Person
類對象,NSKVONotifyin_Person
實際上是Person
的子類,那麼也就是說其superclass
指針是指向Person
類對象的,NSKVONotifyin_Person
是runtime
在運行時生成的。那麼p1對象在調用setAge:
方法的時候,確定會根據p1的isa
找到NSKVONotifyin_Person
,在 NSKVONotifyin_Person
中找setAge:
的方法及實現。orm
通過查閱資料咱們能夠了解到。 NSKVONotifyin_Person
中的setAge
方法中其實調用了Fundation
框架中C語言函數 _NSsetIntValueAndNotify
,_NSsetIntValueAndNotify
內部作的操做至關於,首先調用willChangeValueForKey
將要改變方法,以後調用父類的setAge
方法對成員變量賦值,最後調用didChangeValueForKey
已經改變方法。didChangeValueForKey
中會調用監聽器的監聽方法,最終來到監聽者的observeValueForKeyPath
方法中。
首先通過以前打斷點打印isa
指針,咱們已經驗證了,在執行添加監聽的方法時,會將isa
指針指向一個經過runtime
建立的Person
的子類NSKVONotifyin_Person
。 另外咱們能夠經過打印方法實現的地址來看一下p1
和p2
的setAge:
的方法實現的地址在添加KVO先後有什麼變化
// 經過methodForSelector找到方法實現的地址
NSLog(@"添加KVO監聽以前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"添加KVO監聽以後 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
複製代碼
咱們發如今添加KVO
監聽以前,p1
和p2
的setAge:
方法實現的地址相同,而通過KVO
監聽以後,p1
的setAge
方法實現的地址發生了變化,咱們經過打印方法實現來看一下先後的變化發現,確實如咱們上面所講的同樣,p1
的setAge
方法的實現由Person
類方法中的setAge方法轉換爲了C語言的Foundation
框架的_NSsetIntValueAndNotify
函數。
Foundation
框架中會根據屬性的類型,調用不一樣的方法。例如咱們以前定義的int
類型的age
屬性,那麼咱們看到Foundation
框架中調用的_NSsetIntValueAndNotify
函數。那麼咱們把age
的屬性類型變爲double
從新打印一遍
咱們發現調用的函數變爲了_NSSetDoubleValueAndNotify
,那麼這說明Foundation
框架中有許多此類型的函數,經過屬性的不一樣類型調用不一樣的函數。 那麼咱們能夠推測Foundation
框架中還有不少例如_NSSetBoolValueAndNotify
、_NSSetCharValueAndNotify
、_NSSetFloatValueAndNotify
、_NSSetLongValueAndNotify
等等函數。 咱們能夠找到Foundation
框架文件,經過命令行查詢關鍵字找到相關函數
首先咱們知道,NSKVONotifyin_Person
做爲Person
的子類,其superclass
指針指向Person
類,而且NSKVONotifyin_Person
內部必定對setAge
方法作了單獨的實現,那麼NSKVONotifyin_Person
同Person
類的差異可能就在於其內存儲的對象方法及實現不一樣。 咱們經過runtime
分別打印Person
類對象和NSKVONotifyin_Person
類對象內存儲的對象方法
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
p1.age = 1.0;
Person *p2 = [[Person alloc] init];
p1.age = 2.0;
// self 監聽 p1的 age屬性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self printMethods: object_getClass(p2)];
[self printMethods: object_getClass(p1)];
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void) printMethods:(Class)cls
{
unsigned int count ;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0 ; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString: methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@",methodNames);
free(methods);
}
複製代碼
上述打印內容以下
經過上述代碼咱們發現NSKVONotifyin_Person
中有4個對象方法。分別爲setAge:
、 class
、 dealloc
、_isKVOA
, 那麼至此咱們能夠畫出NSKVONotifyin_Person
的內存結構以及方法調用順序。
這裏NSKVONotifyin_Person
重寫class
方法是爲了隱藏NSKVONotifyin_Person
。不被外界所看到。咱們在p1添加過KVO監聽以後,分別打印p1和p2對象的class
能夠發現他們都返回Person
。
NSLog(@"%@,%@",[p1 class],[p2 class]);
// 打印結果 Person,Person
複製代碼
若是NSKVONotifyin_Person
不重寫class
方法,那麼當對象要調用class
對象方法的時候就會一直向上找來到NSObject
,而NSObject
的class
的實現大體爲返回本身isa
指向的類,返回p1的isa指向的類那麼打印出來的類就是NSKVONotifyin_Person
,可是apple
不但願將NSKVONotifyin_Person
類暴露出來,而且不但願咱們知道NSKVONotifyin_Person
內部實現,因此在內部重寫了class
類,直接返回Person
類,因此外界在調用p1
的class
對象方法時,是Person
類。這樣p1給外界的感受p1仍是Person
類,並不知道NSKVONotifyin_Person
子類的存在。
那麼咱們能夠猜想NSKVONotifyin_Person
內重寫的class
內部實現大體爲
- (Class) class {
// 獲得類對象,在找到類對象父類
return class_getSuperclass(object_getClass(self));
}
複製代碼
咱們在Person
類中重寫willChangeValueForKey
和didChangeValueForKey
方法,模擬他們的實現。
- (void)setAge:(int)age
{
NSLog(@"setAge:");
_age = age;
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
複製代碼
再次運行來查看didChangeValueForKey
的方法內運行過程,經過打印內容能夠看到,確實在didChangeValueForKey
方法內部已經調用了observer
的observeValueForKeyPath:ofObject:change:context
方法。
** 當一個對象使用了KVO
監聽,iOS系統會修改這個對象的isa
指針,改成指向一個全新的經過Runtime
動態建立的子類,子類擁有本身的set
方法實現,set方法實現內部會順序調用willChangeValueForKey
方法、原來的setter
方法實現、didChangeValueForKey
方法,而didChangeValueForKey
方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context
監聽方法。**
被監聽的屬性的值被修改時,就會自動觸發KVO。若是想要手動觸發KVO
,則須要咱們本身調用willChangeValueForKey
和didChangeValueForKey
方法便可在不改變屬性值的狀況下手動觸發KVO,而且這兩個方法缺一不可。
經過如下代碼能夠驗證
Person *p1 = [[Person alloc] init];
p1.age = 1.0;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
[p1 removeObserver:self forKeyPath:@"age"];
複製代碼
經過打印咱們能夠發現,didChangeValueForKey
方法內部成功調用了observeValueForKeyPath:ofObject:change:context:
,而且age的值並無發生改變。