KVO的原理是什麼?底層是如何實現的? 我們可以通過代碼去探索一下。 創建自定義類:XGPerson 我們的思路就是看看對象添加KVO之前和之後有什麼變化,是否有區別,代碼如下: 輸出: 從上面可以看出,object_getClass 和 class 方式分別獲取到的 類對象竟然不一樣,在對象添加了 ...
KVO的原理是什麼?底層是如何實現的?
我們可以通過代碼去探索一下。
創建自定義類:XGPerson
@interface XGPerson : NSObject @property (nonatomic,assign) int age; @property (nonatomic,copy) NSString* name; @end
我們的思路就是看看對象添加KVO之前和之後有什麼變化,是否有區別,代碼如下:
@interface ViewController () @property (strong, nonatomic) XGPerson *person1; @property (strong, nonatomic) XGPerson *person2; @end - (void)viewDidLoad { [super viewDidLoad]; self.person1 = [[XGPerson alloc]init]; self.person2 = [[XGPerson alloc]init]; self.person1.age = 1; self.person2.age = 10; // 添加監聽之前,獲取類對象,通過兩種方式分別獲取 p1 和 p2的類對象 NSLog(@"before getClass--->> p1:%@ p2:%@",object_getClass(self.person1),object_getClass(self.person2)); NSLog(@"before class--->> p1:%@ p2:%@",[self.person1 class],[self.person2 class]); // 添加KVO監聽 NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil]; // 添加監聽之後,獲取類對象 NSLog(@"after getClass--->> p1:%@ p2:%@",object_getClass(self.person1),object_getClass(self.person2)); NSLog(@"after class--->> p1:%@ p2:%@",[self.person1 class],[self.person2 class]); }
輸出:
2018-11-02 15:16:13.276167+0800 KVO原理[4083:170379] before getClass--->> p1:XGPerson p2:XGPerson 2018-11-02 15:16:13.276271+0800 KVO原理[4083:170379] before class--->> p1:XGPerson p2:XGPerson 2018-11-02 15:16:13.276712+0800 KVO原理[4083:170379] after getClass--->> p1:NSKVONotifying_XGPerson p2:XGPerson 2018-11-02 15:16:13.276815+0800 KVO原理[4083:170379] after class--->> p1:XGPerson p2:XGPerson
從上面可以看出,object_getClass 和 class 方式分別獲取到的 類對象竟然不一樣,在對象添加了KVO之後,使用object_getClass的方式獲取到的對象和我們自定義的對象不一樣,而是NSKVONotifying_XGPerson,可以懷疑 class 方法可能被篡改了.
最終發現NSKVONotifying_XGPerson是使用Runtime動態創建的一個類,是XGPerson的子類.
看完對象,接下來我們來看下屬性,就是被我們添加了KVO的屬性age,我們要觸發KVO回調就是去給age設置個值,那它肯定就是調用setAge這個方法.
下麵監聽下這個方法在被添加了KVO之後有什麼不一樣.
NSLog(@"person1添加KVO監聽之前 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]); // 添加KVO監聽 NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil]; NSLog(@"person1添加KVO監聽之後 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
輸出:
2018-11-02 15:16:13.276402+0800 KVO原理[4083:170379] person1添加KVO監聽之前 - 0x10277c3e0 0x10277c3e0 2018-11-02 15:16:17.031319+0800 KVO原理[4083:170379] person1添加KVO監聽之後 - 0x102b21f8e 0x10277c3e0
看輸出我們能發現,在監聽之前兩個對象的方法所指向的物理地址都是一樣的,添加監聽後,person1對象的setAge方法就變了,這就說明一個問題,這個方法的實現變了,我們再通過Xcode斷點調試列印看下到底調用什麼方法
斷點後,在調試器中使用 po 列印對象
(lldb) po [self.person1 methodForSelector:@selector(setAge:)]
(Foundation`_NSSetIntValueAndNotify)
(lldb) po [self.person2 methodForSelector:@selector(setAge:)]
(KVO原理`-[XGPerson setAge:] at XGPerson.m:13)
通過輸出結果可以發現person1的setAge已經被重寫了,改成了調用Foundation框架中C語言寫的 _NSSetIntValueAndNotify 方法,
還有一點,監聽的屬性值類型不同,調用的方法也不同,如果是NSString的,就會調用 _NSSetObjectValueAndNotify 方法,會有幾種類型
大家都知道蘋果的代碼是不開源的,所以我們也不知道 _NSSetIntValueAndNotify 這個方法裡面到底調用了些什麼,那我們可以試著通過其它的方式去猜一下裡面是怎麼調用的。
KVO底層的調用順序
我們先對我們自定義的類下手,重寫下類裡面的幾個方法:
類實現:
#import "XGPerson.h"
@implementation XGPerson
- (void)setAge:(int)age{
_age = age;
NSLog(@"XGPerson setAge");
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
重寫上面3個方法來監聽我們的值到底是怎麼被改的,KVO的通知回調又是什麼時候調用的
我們先設置KVO的監聽回調
// KVO監聽回調 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"監聽到%@的%@屬性值改變了 - %@", object, keyPath, change[@"new"]); }
我們直接修改person1的age值,觸發一下KVO,輸出如下:
2018-11-02 15:38:24.788395+0800 KVO原理[4298:186471] willChangeValueForKey 2018-11-02 15:38:24.788573+0800 KVO原理[4298:186471] XGPerson setAge 2018-11-02 15:38:24.788696+0800 KVO原理[4298:186471] didChangeValueForKey - begin 2018-11-02 15:38:24.788893+0800 KVO原理[4298:186471] 監聽到<XGPerson: 0x60400022f420>的age屬性值改變了 - 2 2018-11-02 15:38:24.789014+0800 KVO原理[4298:186471] didChangeValueForKey - end
從結果中可以看出KVO是在哪個時候觸發回調的,就是在 didChangeValueForKey 這個方法裡面觸發的
NSKVONotifying_XGPerson子類的研究
接下來我們再來研究下之前上面說的那個 NSKVONotifying_XGPerson 子類,可能大家會很好奇這裡面到底有些什麼東西,下麵我們就使用runtime將這個子類的所有方法都列印出來
我們先寫一個方法用來列印一個類對象的所有方法,代碼如下:
// 獲取一個對象的所有方法 - (void)getMehtodsOfClass:(Class)cls{ unsigned int count; Method* methods = class_copyMethodList(cls, &count); NSMutableString* methodList = [[NSMutableString alloc]init]; for (int i=0; i < count; i++) { Method method = methods[i]; NSString* methodName = NSStringFromSelector(method_getName(method)); [methodList appendString:[NSString stringWithFormat:@"| %@",methodName]]; } NSLog(@"%@對象-所有方法:%@",cls,methodList);
// C語言的函數是需要手動釋放記憶體的喔
free(methods);
}
下麵使用這個方法列印下person1的所有方法,順便我們再對比下 object_getClass 和 class
// 一定要使用 object_getClass去獲取類對象,不然獲取到的不是真正的那個子類,而是XGPperson這個類 [self getMehtodsOfClass:object_getClass(self.person1)];
// 使用 class屬性獲取的類對象 [self getMehtodsOfClass:[self.person1 class]];
輸出:
2018-11-02 15:45:07.918209+0800 KVO原理[4369:190437] NSKVONotifying_XGPerson對象-所有方法:| setAge:| class| dealloc| _isKVOA 2018-11-02 15:45:07.918371+0800 KVO原理[4369:190437] XGPerson對象-所有方法:| .cxx_destruct| name| willChangeValueForKey:| didChangeValueForKey:| setName:| setAge:| age
通過結果可以看出,這個子類裡面就是重寫了3個父類方法,還有一個私有的方法,我們XGPerson這個類還有一個name屬性,這裡為什麼沒有setName呢?因為我們沒有給 name 屬性添加KVO,所以就不會重寫它,這裡面確實有那個 class 方法,確實被重寫了,所以當我們使用 [self.person1 class] 的方式的時候它內部怎麼返回的就清楚了。
NSKVONotifying_XGPerson 偽代碼實現
通過上面的研究,我們大概也能清楚NSKVONotifying_XGPerson這個子類裡面是如何實現的了,大概的代碼如下:
頭文件:
@interface NSKVONotifying_XGPerson : XGPerson @end
實現:
#import "NSKVONotifying_XGPerson.h" // KVO的原理偽代碼實現 @implementation NSKVONotifying_XGPerson - (void)setAge:(int)age{ _NSSetIntValueAndNotify(); } - (void)_NSSetIntValueAndNotify{ // KVO的調用順序 [self willChangeValueForKey:@"age"]; [super setAge:age]; // KVO會在didChangeValueForKey裡面調用age屬性變更的通知回調 [self didChangeValueForKey:@"age"]; } - (void)didChangeValueForKey:(NSString *)key{
// 通知監聽器,某某屬性值發生了改變 [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil]; } // 會重寫class返回父類的class // 原因:1.為了隱藏這個動態的子類 2.為了讓開發者不那麼迷惑 - (Class)class{ return [XGPerson class]; } - (void)dealloc{ // 回收工作 } - (BOOL)_isKVOA{ return YES; }
如何手動調用KVO
其實通過上面的代碼大家已經知道了KVO是怎麼觸發的了,那怎麼手動調用呢?很簡單,只要調用兩個方法就行了,如下:
[self.person1 willChangeValueForKey:@"age"]; [self.person1 didChangeValueForKey:@"age"];
但是上面說調用順序的時候,好像明明KVO是在 didChangeVlaueForKey 裡面調用的,為什麼還要調用 willChangeVlaueForKey呢?
那是因為KVO調用的時候會去判斷這個對象有沒有調用 willChangeVlaueForKey 只有調用了這個之後,再調用 didChangeVlaueForKey 才能真正觸發KVO
總結
KVO是通過runtime機制動態的給要添加KVO監聽的對象創建一個子類,並且讓instance對象的isa指向這個全新的子類.
當修改instance對象的屬性時,會調用Foundation的_NSSetXXXValueAndNotify函數,順序如下:
- willChangeValueForKey:
- 父類原來的setter
- didChangeValueForKey:
didChangeValueForKey 內部會觸發監聽器(Oberser)的監聽方法( observeValueForKeyPath:ofObject:change:context:)
通過這個子類重寫一些父類的方法達到觸發KVO回調的目的.
補充
KVO是使用了典型的發佈訂閱者設計模式實現事件回調的功能,多個訂閱者,一個發佈者,簡單的實現如下:
1> 訂閱者向發佈者進行訂閱.
2> 發佈者將訂閱者信息保存到一個集合中.
3> 當觸發事件後,發佈者就遍歷這個集合分別調用之前的訂閱者,從而達到1對多的通知.
以上已全部完畢,如有什麼不正確的地方大家可以指出~~ ^_^ 下次再見~~