【iOS 底層原理】KVO 本質(zhì)原理

一、KVO 的使用

KVO 的全稱 Key-Value Observing,俗稱“鍵值監(jiān)聽”,可以用于監(jiān)聽某個對象屬性值的改變。

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    // self 監(jiān)聽 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(@"監(jiān)聽到%@的%@改變了%@", object, keyPath,change);
}

// 打印內(nèi)容
監(jiān)聽到<Person: 0x604000205460>的age改變了{
    kind = 1;
    new = 10;
    old = 2;
}

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

二、KVO 底層實現(xiàn)原理

首先我們對上述代碼中添加監(jiān)聽的地方打斷點,看觀察一下,addObserver 方法對 p1 對象做了什么處理?也就是說 p1 對象在經(jīng)過 addObserver 方法之后發(fā)生了什么改變,我們通過打印isa指針如下圖所示

p1 對象進行 KVO 監(jiān)聽,p2 對象不進行 KVO 監(jiān)聽

image.png

通過上圖我們發(fā)現(xiàn),p1對象執(zhí)行過 addObserver 操作之后,p1對象的isa指針由之前的指向類對象Person變?yōu)橹赶騈SKVONotifyin_Person類對象,而p2對象沒有任何改變。也就是說一旦p1對象添加了KVO監(jiān)聽以后,其isa指針就會發(fā)生變化,因此set方法的執(zhí)行效果就不一樣了。

那么我們先來觀察p2對象在內(nèi)容中是如何存儲的,然后對比p2來觀察p1。 首先我們知道,p2在調(diào)用setage方法的時候,首先會通過p2對象中的isa指針找到Person類對象,然后在類對象中找到setage方法。然后找到方法對應的實現(xiàn)。如下圖所示

image.png

但是剛才我們發(fā)現(xiàn)p1對象的isa指針在經(jīng)過KVO監(jiān)聽之后已經(jīng)指向了NSKVONotifyin_Person類對象,NSKVONotifyin_Person其實是Person的子類,那么也就是說其superclass指針是指向Person類對象的,NSKVONotifyin_Person是runtime在運行時生成的。那么p1對象在調(diào)用setage方法的時候,肯定會根據(jù)p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setage的方法及實現(xiàn)。

經(jīng)過查閱資料我們可以了解到。
NSKVONotifyin_Person中的setage方法中其實調(diào)用了 Fundation框架中C語言函數(shù) _NSSetIntValueAndNotify,_NSSetIntValueAndNotify內(nèi)部做的操作相當于,首先調(diào)用willChangeValueForKey 將要改變方法,之后調(diào)用父類的setage方法對成員變量賦值,最后調(diào)用didChangeValueForKey已經(jīng)改變方法。didChangeValueForKey中會調(diào)用監(jiān)聽器的監(jiān)聽方法,最終來到監(jiān)聽者的observeValueForKeyPath方法中。

image.png

三、驗證過程

首先經(jīng)過之前打斷點打印isa指針,我們已經(jīng)驗證了,在執(zhí)行添加監(jiān)聽的方法時,會將isa指針指向一個通過runtime創(chuàng)建的Person的子類NSKVONotifyin_Person。
另外我們可以通過打印方法實現(xiàn)的地址來看一下p1和p2的setage的方法實現(xiàn)的地址在添加KVO前后有什么變化。

// 通過methodForSelector找到方法實現(xiàn)的地址
NSLog(@"添加KVO監(jiān)聽之前 - 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監(jiān)聽之后 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
image.png

我們發(fā)現(xiàn)在添加KVO監(jiān)聽之前,p1和p2的setAge方法實現(xiàn)的地址相同,而經(jīng)過KVO監(jiān)聽之后,p1的setAge方法實現(xiàn)的地址發(fā)生了變化,我們通過打印方法實現(xiàn)來看一下前后的變化發(fā)現(xiàn),確實如我們上面所講的一樣,p1的setAge方法的實現(xiàn)由Person類方法中的setAge方法轉(zhuǎn)換為了C語言的Foundation框架的_NSsetIntValueAndNotify函數(shù)。

Foundation框架中會根據(jù)屬性的類型,調(diào)用不同的方法。例如我們之前定義的int類型的age屬性,那么我們看到Foundation框架中調(diào)用的_NSsetIntValueAndNotify函數(shù)。那么我們把age的屬性類型變?yōu)閐ouble重新打印一遍

image.png

我們發(fā)現(xiàn)調(diào)用的函數(shù)變?yōu)榱薩NSSetDoubleValueAndNotify,那么這說明Foundation框架中有許多此類型的函數(shù),通過屬性的不同類型調(diào)用不同的函數(shù)。
那么我們可以推測Foundation框架中還有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函數(shù)。

我們可以找到Foundation框架文件,通過命令行查詢關(guān)鍵字找到相關(guān)函數(shù)

image.png

四、NSKVONotifyin_XXX 內(nèi)部結(jié)構(gòu)

首先我們知道,NSKVONotifyin_Person作為Person的子類,其superclass指針指向Person類,并且NSKVONotifyin_Person內(nèi)部一定對setAge方法做了單獨的實現(xiàn),那么NSKVONotifyin_Person同Person類的差別可能就在于其內(nèi)存儲的對象方法及實現(xiàn)不同。
我們通過runtime分別打印Person類對象和NSKVONotifyin_Person類對象內(nèi)存儲的對象方法

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p1 = [[Person alloc] init];
    p1.age = 1.0;
    Person *p2 = [[Person alloc] init];
    p1.age = 2.0;
    // self 監(jiān)聽 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);
}

上述打印內(nèi)容如下


image.png

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

image.png

這里NSKVONotifyin_Person重寫class方法是為了隱藏NSKVONotifyin_Person。不被外界所看到。我們在p1添加過KVO監(jiān)聽之后,分別打印p1和p2對象的class可以發(fā)現(xiàn)他們都返回Person。

NSLog(@"%@,%@",[p1 class],[p2 class]);
// 打印結(jié)果 Person,Person

如果NSKVONotifyin_Person不重寫class方法,那么當對象要調(diào)用class對象方法的時候就會一直向上找來到nsobject,而nsobect的class的實現(xiàn)大致為返回自己isa指向的類,返回p1的isa指向的類那么打印出來的類就是NSKVONotifyin_Person,但是apple不希望將NSKVONotifyin_Person類暴露出來,并且不希望我們知道NSKVONotifyin_Person內(nèi)部實現(xiàn),所以在內(nèi)部重寫了class類,直接返回Person類,所以外界在調(diào)用p1的class對象方法時,是Person類。這樣p1給外界的感覺p1還是Person類,并不知道NSKVONotifyin_Person子類的存在。

那么我們可以猜測NSKVONotifyin_Person內(nèi)重寫的class內(nèi)部實現(xiàn)大致為

- (Class) class {
     // 得到類對象,在找到類對象父類
     return class_getSuperclass(object_getClass(self));
}

五、手動實現(xiàn) KVO

驗證didChangeValueForKey:內(nèi)部會調(diào)用observer的observeValueForKeyPath:ofObject:change:context:方法

- (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的方法內(nèi)運行過程,通過打印內(nèi)容可以看到,確實在didChangeValueForKey方法內(nèi)部已經(jīng)調(diào)用了observer的observeValueForKeyPath:ofObject:change:context:方法。

image.png

六、面試題

iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么?)

答:當一個對象使用了KVO監(jiān)聽,iOS系統(tǒng)會修改這個對象的isa指針,改為指向一個全新的通過Runtime動態(tài)創(chuàng)建的子類,子類擁有自己的set方法實現(xiàn),set方法實現(xiàn)內(nèi)部會順序調(diào)用willChangeValueForKey方法、原來的setter方法實現(xiàn)、didChangeValueForKey方法,而didChangeValueForKey方法內(nèi)部又會調(diào)用監(jiān)聽器的observeValueForKeyPath:ofObject:change:context:監(jiān)聽方法。

如何手動觸發(fā)KVO

答:被監(jiān)聽的屬性的值被修改時,就會自動觸發(fā)KVO。如果想要手動觸發(fā)KVO,則需要我們自己調(diào)用willChangeValueForKey和didChangeValueForKey方法即可在不改變屬性值的情況下手動觸發(fā)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"];
image.png

通過打印我們可以發(fā)現(xiàn),didChangeValueForKey方法內(nèi)部成功調(diào)用了observeValueForKeyPath:ofObject:change:context:,并且age的值并沒有發(fā)生改變。

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

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

  • 問題 iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么?) 如何手動觸發(fā)KVO ? 首先需要了解KVO...
    hjltony閱讀 625評論 0 2
  • 對小碼哥底層班視頻學習的總結(jié)與記錄。面試題部分,通過對面試題的分析探索問題的本質(zhì)內(nèi)容。 問題iOS用什么方式實現(xiàn)對...
    xx_cc閱讀 10,951評論 26 65
  • 關(guān)于KVO,首先我們來看兩道面試題 KVO的本質(zhì)是什么?如何手動去觸發(fā)KVO?直接修改成員變量會觸發(fā)KVO么? 怎...
    Mark_Guan閱讀 1,473評論 0 9
  • 一只白仙鶴 飛呀飛呀 飛到了那山坡 一個美麗的少女 冰涼的夢里 睡著一只白仙鶴 靈魂在看著 看著那朵已凋零的百合 ...
    吉羊玉奕v閱讀 980評論 18 40
  • 為什么來競選? 因為我想做的更多,想承擔更多的責任??偨Y(jié)下來有三點: 第一, 想做事,作為會員,我有一顆服務(wù)會員的...
    Wangwei2017閱讀 525評論 0 1

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