iOS底層原理探索—KVO的本質(zhì)

探索底層原理,積累從點(diǎn)滴做起。大家好,我是Mars。

往期回顧

iOS底層原理探索—OC對象的本質(zhì)
iOS底層原理探索—class的本質(zhì)
今天帶領(lǐng)大家探索iOS之KVO的本質(zhì)。

KVO

KVO全稱Key-Value Observing,鍵值監(jiān)聽

KVO是OC對觀察者設(shè)計(jì)模式的一種實(shí)現(xiàn),注冊一個觀察者時,調(diào)用addObserver: forKeyPath:options: context:,觀察者觀察A的屬性,系統(tǒng)在運(yùn)行時,動態(tài)創(chuàng)建一個NSKVONotifying_A類,將A的isa指針指向這個類。NSKVONotifying_A是原來類的子類,來重寫原來類的setter方法。這段話相信在很多博客中都看到過,當(dāng)然這樣回答KVO的本質(zhì)是正確,今天我們就圍繞這段話,來探索KVO的本質(zhì)。

KVO監(jiān)聽age屬性變化.png

上述代碼中可以看出,在添加監(jiān)聽之后,當(dāng)person1的age屬性的值在發(fā)生改變時,就會通知到監(jiān)聽者,執(zhí)行監(jiān)聽者的observeValueForKeyPath方法。

我們知道,OC中賦值的操作都是調(diào)用了對象的set方法,我們重寫了Person類的setAge方法之后,加入斷點(diǎn),重新運(yùn)行點(diǎn)擊屏幕后發(fā)現(xiàn),person1和person2對象都調(diào)用了setAge方法,person1由于增加了監(jiān)聽,執(zhí)行完 setAge方法之后還會執(zhí)行監(jiān)聽器的observeValueForKeyPath方法。

這就說明KVO在運(yùn)行時對person1對象做了一些操作,從而使調(diào)用了setAge方法之后執(zhí)行其他方法,究竟做了一些什么改變呢?

為了驗(yàn)證這個問題,我們在給person1添加監(jiān)聽器之前加入查看person1和person2的類對象的代碼:

    NSLog(@"person1添加KVO監(jiān)聽之前 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));

在給person1添加監(jiān)聽器之后加入查看person1和person2的類對象的代碼:

    NSLog(@"person1添加KVO監(jiān)聽之后 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));

打印輸出的結(jié)果為:

person1添加KVO監(jiān)聽之前 - Person Person
person1添加KVO監(jiān)聽之后 - NSKVONotifying_Person Person

經(jīng)過試驗(yàn)分析我們發(fā)現(xiàn),person1對象經(jīng)過添加監(jiān)聽操作之后,person1對象的isa指針由之前的指向類對象Person變?yōu)橹赶?code>NSKVONotifyin_Person類對象,而person2對象的isa指針指向沒有任何改變。也就是說一旦person1對象添加了KVO監(jiān)聽以后,其isa指針就會發(fā)生變化,因此setAge方法的執(zhí)行效果就不一樣了。

我們可以分析person2對象在內(nèi)存中是如何存儲的,然后通過對比person1和person2來進(jìn)一步分析KVO的底層實(shí)現(xiàn)。

首先我們知道,person2在調(diào)用setAge方法的時候,首先會通過person2對象中的isa指針找到Person類對象,然后在類對象中找到setAge方法,然后找到方法對應(yīng)的實(shí)現(xiàn):

未添加KVO監(jiān)聽的對象.jpeg

但是person1對象的isa指針的指向已經(jīng)在添加了KVO之后發(fā)生了改變,指向了NSKVONotifyin_Person這個類對象。NSKVONotifyin_Person其實(shí)是Person的子類,NSKVONotifyin_Personisa指針指向Person。所以person1對象在調(diào)用setAge方法時,會根據(jù)自己的isa指針先找到NSKVONotifyin_Person這個類,在NSKVONotifyin_Person這個類中找到setAge方法的相關(guān)實(shí)現(xiàn):

添加了KVO監(jiān)聽的對象.jpeg

經(jīng)過查看底層源碼和相關(guān)資料分析們可以知道,NSKVONotifyin_Person中的setAge方法中其實(shí)調(diào)用了 Fundation框架中C語言函數(shù)_NSsetIntValueAndNotify,而_NSsetIntValueAndNotify內(nèi)部做的操作相當(dāng)于,首先調(diào)用willChangeValueForKey方法,之后調(diào)用父類的setAge方法對成員變量賦值,最后調(diào)用didChangeValueForKey方法。其中didChangeValueForKey中會調(diào)用監(jiān)聽器的監(jiān)聽方法,最終來到監(jiān)聽者的observeValueForKeyPath方法。

補(bǔ)充
在 Fundation框架中,和_NSsetIntValueAndNotify類似的函數(shù)其實(shí)還有很多,簡單列舉幾個:

Fundation框架中的類似函數(shù).jpeg

根據(jù)函數(shù)名可以知道,這些函數(shù)的調(diào)用取決與添加KVO監(jiān)聽的屬性類型。

NSKVONotifyin_Person的內(nèi)部結(jié)構(gòu)

NSKVONotifyin_Person作為Person的子類,其superclass指針指向Person類,并且NSKVONotifyin_Person內(nèi)部的setAge方法做了單獨(dú)的實(shí)現(xiàn)。我們可以通過runtime的方法去分別打印person1和person2兩個對象和NSKVONotifyin_Person類對象內(nèi)存儲的對象方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
// 給person1對象添加KVO監(jiān)聽
    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,

通過上述試驗(yàn)發(fā)現(xiàn)NSKVONotifyin_Person中有4個對象方法。分別為setAge:class、dealloc、_isKVOA,我們就可以畫出NSKVONotifyin_Person的內(nèi)存結(jié)構(gòu)以及方法調(diào)用順序:

NSKVONotifyin_Person的內(nèi)存結(jié)構(gòu)及方法調(diào)用順序.jpeg

我們通過代碼打印person1對象添加了KVO監(jiān)聽之后的class發(fā)現(xiàn),返回的仍舊是Person

NSLog(@"%@,%@",[person1 class],[person1 class]);

打印結(jié)果為:

Person,Person

很明顯,NSKVONotifyin_Person是重寫了class方法的,如果沒有重寫的話,返回person1的isa指針指向的類打印結(jié)果應(yīng)該是NSKVONotifyin_Person,但是蘋果官方不希望將NSKVONotifyin_Person類的內(nèi)部實(shí)現(xiàn)暴露出來,所以在內(nèi)部重寫了class方法,直接返回Person類,所以我們在調(diào)用person1的class方法時,返回的是Person類。

至此,我們就可以回答上篇文章預(yù)留的問題了:

1、KVO的本質(zhì)是什么?
當(dāng)我們給對象注冊一個觀察者添加了KVO監(jiān)聽時,系統(tǒng)會修改這個對象的isa指針指向。在運(yùn)行時,動態(tài)創(chuàng)建一個新的子類,NSKVONotifying_A類,將A的isa指針指向這個子類,來重寫原來類的set方法;set方法實(shí)現(xiàn)內(nèi)部會順序調(diào)用willChangeValueForKey方法、原來的setter方法實(shí)現(xiàn)、didChangeValueForKey方法,而didChangeValueForKey方法內(nèi)部又會調(diào)用監(jiān)聽器的observeValueForKeyPath:ofObject:change:context:監(jiān)聽方法。

2、如何手動觸發(fā)KVO?
實(shí)現(xiàn)調(diào)用willChangeValueForKeydidChangeValueForKey方法。

如果本文對你有所幫助,點(diǎn)亮喜歡或者關(guān)注支持一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容