探索底層原理,積累從點滴做起。大家好,我是Mars。
往期回顧
iOS底層原理探索—OC對象的本質
iOS底層原理探索—class的本質
今天帶領大家探索iOS之KVO的本質。
KVO
KVO全稱Key-Value Observing,鍵值監聽。
KVO是OC對觀察者設計模式的一種實現,注冊一個觀察者時,調用addObserver: forKeyPath:options: context:
,觀察者觀察A的屬性,系統在運行時,動態創建一個NSKVONotifying_A
類,將A的isa
指針指向這個類。NSKVONotifying_A
是原來類的子類,來重寫原來類的setter
方法。這段話相信在很多博客中都看到過,當然這樣回答KVO的本質是正確,今天我們就圍繞這段話,來探索KVO的本質。
上述代碼中可以看出,在添加監聽之后,當person1的age
屬性的值在發生改變時,就會通知到監聽者,執行監聽者的observeValueForKeyPath
方法。
我們知道,OC中賦值的操作都是調用了對象的set
方法,我們重寫了Person類的setAge
方法之后,加入斷點,重新運行點擊屏幕后發現,person1和person2對象都調用了setAge
方法,person1由于增加了監聽,執行完 setAge
方法之后還會執行監聽器的observeValueForKeyPath
方法。
這就說明KVO在運行時對person1對象做了一些操作,從而使調用了setAge
方法之后執行其他方法,究竟做了一些什么改變呢?
為了驗證這個問題,我們在給person1添加監聽器之前加入查看person1和person2的類對象的代碼:
NSLog(@"person1添加KVO監聽之前 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
在給person1添加監聽器之后加入查看person1和person2的類對象的代碼:
NSLog(@"person1添加KVO監聽之后 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
打印輸出的結果為:
person1添加KVO監聽之前 - Person Person
person1添加KVO監聽之后 - NSKVONotifying_Person Person
經過試驗分析我們發現,person1對象經過添加監聽操作之后,person1對象的isa
指針由之前的指向類對象Person
變為指向NSKVONotifyin_Person
類對象,而person2對象的isa
指針指向沒有任何改變。也就是說一旦person1對象添加了KVO監聽以后,其isa
指針就會發生變化,因此setAge
方法的執行效果就不一樣了。
我們可以分析person2對象在內存中是如何存儲的,然后通過對比person1和person2來進一步分析KVO的底層實現。
首先我們知道,person2在調用setAge
方法的時候,首先會通過person2對象中的isa
指針找到Person
類對象,然后在類對象中找到setAge
方法,然后找到方法對應的實現:
但是person1對象的isa
指針的指向已經在添加了KVO之后發生了改變,指向了NSKVONotifyin_Person
這個類對象。NSKVONotifyin_Person
其實是Person
的子類,NSKVONotifyin_Person
的isa
指針指向Person
。所以person1對象在調用setAge
方法時,會根據自己的isa
指針先找到NSKVONotifyin_Person
這個類,在NSKVONotifyin_Person
這個類中找到setAge
方法的相關實現:
經過查看底層源碼和相關資料分析們可以知道,NSKVONotifyin_Person
中的setAge
方法中其實調用了 Fundation
框架中C語言函數_NSsetIntValueAndNotify
,而_NSsetIntValueAndNotify
內部做的操作相當于,首先調用willChangeValueForKey
方法,之后調用父類的setAge
方法對成員變量賦值,最后調用didChangeValueForKey
方法。其中didChangeValueForKey
中會調用監聽器的監聽方法,最終來到監聽者的observeValueForKeyPath
方法。
補充
在 Fundation框架中,和_NSsetIntValueAndNotify
類似的函數其實還有很多,簡單列舉幾個:
根據函數名可以知道,這些函數的調用取決與添加KVO監聽的屬性類型。
NSKVONotifyin_Person的內部結構
NSKVONotifyin_Person
作為Person
的子類,其superclass
指針指向Person
類,并且NSKVONotifyin_Person
內部的setAge
方法做了單獨的實現。我們可以通過runtime的方法去分別打印person1和person2兩個對象和NSKVONotifyin_Person
類對象內存儲的對象方法:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethods: object_getClass(self.person1)];
[self printMethods: object_getClass(self.person2)];
[self.person1 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);
}
打印輸出:
MJPerson : setAge:, age,
NSKVONotifying_MJPerson : setAge:, class, dealloc, _isKVOA,
通過上述試驗發現NSKVONotifyin_Person
中有4個對象方法。分別為setAge:
、class
、dealloc
、_isKVOA
,我們就可以畫出NSKVONotifyin_Person
的內存結構以及方法調用順序:
我們通過代碼打印person1對象添加了KVO監聽之后的class發現,返回的仍舊是Person
NSLog(@"%@,%@",[person1 class],[person1 class]);
打印結果為:
Person,Person
很明顯,NSKVONotifyin_Person
是重寫了class
方法的,如果沒有重寫的話,返回person1的isa
指針指向的類打印結果應該是NSKVONotifyin_Person
,但是蘋果官方不希望將NSKVONotifyin_Person
類的內部實現暴露出來,所以在內部重寫了class
方法,直接返回Person
類,所以我們在調用person1的class
方法時,返回的是Person
類。
至此,我們就可以回答上篇文章預留的問題了:
1、KVO的本質是什么?
當我們給對象注冊一個觀察者添加了KVO監聽時,系統會修改這個對象的isa
指針指向。在運行時,動態創建一個新的子類,NSKVONotifying_A
類,將A的isa
指針指向這個子類,來重寫原來類的set
方法;set
方法實現內部會順序調用willChangeValueForKey
方法、原來的setter
方法實現、didChangeValueForKey
方法,而didChangeValueForKey
方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:
監聽方法。
2、如何手動觸發KVO?
實現調用willChangeValueForKey
和didChangeValueForKey
方法。
如果本文對你有所幫助,點亮喜歡或者關注支持一下。