一、iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?)
二、如何手動觸發KVO
複製代碼
KVO: 全稱 Key-Value Observing,俗稱「鍵值監聽」,能夠用於監聽某個對象屬性值的改變。bash
簡單的寫一個KVO代碼: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;
///context能夠暫時爲空
[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對象在調用set age方法的時候可能作了一些額外的操做,因此問題出在對象身上,兩個對象在內存中確定不同,兩個對象可能本質上也不同。接下來探索KVO內部是怎麼實現的。spa
首先咱們對上述代碼中添加監聽的地方打斷點,看觀察一下,addObserver方法對p1對象作了什麼處理?也就是說p1對象在通過addObserver方法以後發生了什麼改變,咱們經過打印isa指針以下圖所示命令行
經過上圖咱們發現,p1對象執行過addObserver操做以後,p1對象的isa指針由以前的指向類對象Person變爲指向NSKVONotifyin_Person類對象,而p2對象沒有任何改變。也就是說一旦p1對象添加了KVO監聽之後,其isa指針就會發生變化,所以set方法的執行效果就不同了。那麼咱們先來觀察p2對象在內容中是如何存儲的,而後對比p2來觀察p1。 首先咱們知道,p2在調用set age方法的時候,首先會經過p2對象中的isa指針找到Person類對象,而後在類對象中找到set age方法。而後找到方法對應的實現。以下圖所示3d
可是剛纔咱們發現p1對象的isa指針在通過KVO監聽以後已經指向了NSKVONotifyin_Person類對象,NSKVONotifyin_Person實際上是Person的子類,那麼也就是說其superclass指針是指向Person類對象的,NSKVONotifyin_Person是runtime在運行時生成的。那麼p1對象在調用set age方法的時候,確定會根據p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找set age的方法及實現。通過查閱資料咱們能夠了解到。 NSKVONotifyin_Person中的set age方法中其實調用了 Fundation框架中C語言函數 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify內部作的操做至關於,首先調用willChangeValueForKey 將要改變方法,以後調用父類的setage方法對成員變量賦值,最後調用didChangeValueForKey已經改變方法。didChangeValueForKey中會調用監聽器的監聽方法,最終來到監聽者的observeValueForKeyPath方法中。指針
首先通過以前打斷點打印isa指針,咱們已經驗證了,在執行添加監聽的方法時,會將isa指針指向一個經過runtime建立的Person的子類NSKVONotifyin_Person。 另外咱們能夠經過打印方法實現的地址來看一下p1和p2的set age的方法實現的地址在添加KVO先後有什麼變化。code
// 經過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的set Age方法實現的地址相同,而通過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));
}
複製代碼
驗證didChangeValueForKey:內部會調用observer的observeValueForKeyPath:ofObject:change:context:方法
咱們在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:方法。
至此,咱們就能夠回答文章開頭的那兩個問題了。
一、iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?)
答. 當一個對象使用了KVO監聽,iOS系統會修改這個對象的isa指針,改成指向一個全新的經過Runtime動態建立的子類,子類擁有本身的set方法實現,set方法實現內部會順序調用willChangeValueForKey方法、原來的setter方法實現、didChangeValueForKey方法,而didChangeValueForKey方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:監聽方法。
二、如何手動觸發KVO
答. 被監聽的屬性的值被修改時,就會自動觸發KVO。若是想要手動觸發KVO,則須要咱們本身調用willChangeValueForKey和didChangeValueForKey方法便可在不改變屬性值的狀況下手動觸發KVO,而且這兩個方法缺一不可。
複製代碼
備註:目前仍是博客的搬運工,先積累。