20-KVO分析

前言

什么是KVO(Key-Value Observing)

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
鍵值觀察是一種機(jī)制,它允許對象在其他對象的指定屬性發(fā)生更改時收到通知。

KVO官方地址

KVO基礎(chǔ)

KVO 從日常的開發(fā)中看出無非就是三個api

  • (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
  • (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
  • (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

那么接下來就具體看看這幾個API到底有何作用。

1、NSKeyValueObservingOptions 的作用。

NSKeyValueObservingOptionOldNSKeyValueObservingOptionNew 是我們常用的兩個選選項(xiàng)。
下面通過一個 demo 來驗(yàn)證這個到底有什么作用
先準(zhǔn)備如下一份代碼

@interface CDPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, copy) NSArray *array;
@end

///實(shí)現(xiàn)如下一份代碼
- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.person = [CDPerson alloc];
    self.person.nick = @"Hello"; 
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionOld) context:NULL];
  ///  [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
  ///  [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionPrior) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change = %@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nick = [self.person.nick stringByAppendingString:@"+"];
}

這時候我們分別監(jiān)聽幾個不同的 options ,可以得到如下的結(jié)果

  1. NSKeyValueObservingOptionOld
change = {
    kind = 1;
    old = Hello;
}
  1. NSKeyValueObservingOptionNew
change = {
    kind = 1;
    new = "Hello+";
}
  1. NSKeyValueObservingOptionPrior
change = {
    kind = 1;
    notificationIsPrior = 1;
}
change = {
    kind = 1;
}

2、 context

上下文。這種設(shè)計(jì)在很多場景都有實(shí)用,特別是在CFCG等框架的時候。而從官方文檔上來看就是 :

一種更安全、更可擴(kuò)展的方法是使用上下文來確保您收到的通知是發(fā)送給您的觀察者而不是超類的。
那么我們來驗(yàn)證一下

static void * personName = @"personName";
/// 2、驗(yàn)證 context
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:personName];
    

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == personName) {
        NSLog(@"%@", context);
    }
    NSLog(@"change = %@", change);
}

打印結(jié)果如下:

2021-07-29 23:02:04.270060+0800 001---KVO初探[10373:973646] change = {
    kind = 1;
    new = "Hello+";
}

2021-07-29 23:02:04.270130+0800 001---KVO初探[10373:973646] personName
2021-07-29 23:02:04.270192+0800 001---KVO初探[10373:973646] change = {
    kind = 1;
    new = "niubi-";
}

通過結(jié)果我們發(fā)現(xiàn),這個context 確實(shí)可以被帶到通知里面去。這樣我們就可以更加好判斷誰監(jiān)聽的誰。也可以保證在移除觀察者的時候不會出現(xiàn)問題(不會把父類相同的監(jiān)聽給移除了)。

// 這樣,即使父類也有一個觀察了name 的觀察者,只要context 不一樣,就不會隨意的移除掉。
[self.person removeObserver:self forKeyPath:@"name" context:personName]

3、要不要移除觀察者

通常來說,我們注冊的觀察者一旦執(zhí)行了 dealloc 以后,那么被觀察的對象也就釋放了。所以移除與否都沒有關(guān)系。但是有一些情況是,雖然我的觀察者釋放了,但是這個被觀察的對象依然還存在,那這個時候在給這個觀察者發(fā)生通知那就會出問題了。比如我們上面的被觀察的對象是個單列,或者其他一些暫時沒辦法釋放的東西,那么下次在給當(dāng)前對象發(fā)生通知就會觸發(fā)野指針而崩潰。
所以,最好還是在我們觀察者 dealloc 的時候,執(zhí)行 remove。

4、手動和自動監(jiān)聽KVO

api 里面還有一個 +automaticallyNotifiesObserversForKey:方法,這個方法默認(rèn)返回 true。也就是默認(rèn)開啟自動發(fā)送通知,如果我們返回 false 那么久沒發(fā)自動發(fā)送通知,需要手動發(fā)送通知,即調(diào)用 willChangeValueForKey:and didChangeValueForKey: 者兩個方法來手動發(fā)出通知。也可以通過 + (BOOL)automaticallyNotifiesObserversOfName 這個方法來指定某個屬性是和否可以自動發(fā)出通知(這個要在automaticallyNotifiesObserversForKey:沒有重寫的情況下)。

// 自動開關(guān)關(guān)閉
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return false;
}

當(dāng)我們重寫了如上的方法后,整個類的KVO 就不會自動觸發(fā)通知的發(fā)送。這個時候就需要手動去觸發(fā):

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

5、監(jiān)聽集合類型

如果我們要監(jiān)聽集合類型的屬性(如:NSArray),那么我們實(shí)現(xiàn)如下監(jiān)聽。

  [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"array" options:(NSKeyValueObservingOptionNew) context:NULL];
    

如果直接改變數(shù)組的成員是不會觸發(fā)的,只有按照KVC 的方式去觸發(fā)才可以觸發(fā)通知的發(fā)送。

/// 這樣是不會生效的
[self.person.dateArray addObject:@"222"];

/// 需要下面這樣
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"222"];
[[self.person mutableArrayValueForKey:@"array"] addObject:@"333"];
// 亦或者 
    [[self.person mutableArrayValueForKey:@"dateArray"] removeObject:@"2"];
    [[self.person mutableArrayValueForKey:@"array"] removeObject:@"3"];

當(dāng)然這樣執(zhí)行集合類型的觀察在配合 options 可以看看是什么效果,閣下可以自己去嘗試看看結(jié)果是如何的。筆者這里就不在細(xì)說,還有包括KVC 的相關(guān)的一些對應(yīng)的情況,可以查閱筆者關(guān)于KVC 的表述

6、監(jiān)聽keyPath 多級路徑

self.person.st = [LGStudent alloc];
    self.person.st.name = @"student";
[self.person addObserver:self forKeyPath:@"st.name" options:(NSKeyValueObservingOptionNew) context:NULL]

//執(zhí)行如下方法
self.person.st.name = [self.person.st.name stringByAppendingString:@"+"];

///打印結(jié)果如下:
change = {
    kind = 1;
    new = "student+";
}
change = {
    kind = 1;
    new = "student++";
}

KVO 實(shí)現(xiàn)

KVO 到底是如何實(shí)現(xiàn)的,接下來我們就去探索。這里借助LLDBapi 來一起驗(yàn)證。

1、探索isa

Automatic key-value observing is implemented using a technique called isa-swizzling.
從官方文檔來看,自動KVO是一種 isa-swizzling,那么我們就先來看看這個isa 到底是什么,如下實(shí)現(xiàn)一段代碼,并且下一個斷點(diǎn),分別在添加觀察者和添加后打印結(jié)果

查看isa

從結(jié)果我們可以看出,在添加了觀察者后,isa指向了一個 名為 NSKVONotifying_LGPerson 的類。那么這個類和我們的 LGPerson 有什么關(guān)系呢?那么結(jié)合我們前面類的原理里面探索的,類結(jié)構(gòu)的第二個成員變量是 superClass ,可以得出他們是父子關(guān)系。

(lldb) po 0x00000001c28f8628
NSObject

(lldb) po 0x0000000104a55650
LGPerson

7、NSKVONotifying_CDPerson 里面有什么東西<成員變量、方法、協(xié)議>

這里筆者采用api來看看當(dāng)前這個類里面到底有什么。
接下來調(diào)用如下一個方法來探索這個類里面有什么成員。

- (void)getAllMethodFromCls:(Class)cls {
    
    unsigned int count;
    Method *ms = class_copyMethodList(cls, &count);
    NSLog(@"**************** 方法: %@ : %d ****************", cls, count);
    for (int i = 0; i < count; i++) {
        SEL sel = method_getName(ms[I]);
        NSLog(@"SEL = %@", NSStringFromSelector(sel));
    }
     
    Ivar *ivs = class_copyIvarList(cls, &count);
    NSLog(@"**************** 成員變量: %@ : %d", cls, count);
    for (int i = 0; i < count; i++) {
        const char *cName = ivar_getName(ivs[I]);
        NSLog(@"Name = %@", [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]);
    }
    
    objc_property_t *ps = class_copyPropertyList(cls, &count);
    NSLog(@"**************** 屬性: %@ : %d", cls, count);
    for (int i = 0; i < count; i++) {
        const char *cName = property_getName(ps[I]);
        NSLog(@"Name = %@", [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]);
    }
    NSLog(@"\n\n");
     
}

然后在監(jiān)聽前后監(jiān)聽后分別查看這個類的相關(guān)信息

[self getAllMethodFromCls:object_getClass(self.person)];
   
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"st.name" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    [self getAllMethodFromCls:object_getClass(self.person)];
    

這里筆者有個問題是設(shè)個 st.name 到底是在何處監(jiān)聽的?

觀察前的結(jié)果
觀察后的結(jié)果

從結(jié)果我們可以看到,并沒有setsSt.name 這樣的方法。只有一個 setSt:的方法,這就讓我懷疑是不是 LGStudent 也有創(chuàng)建了一個動態(tài)了的類,而這種多級監(jiān)聽最后只是通過kvc 傳遞到了里面相關(guān)的對象里面去了。
通過調(diào)試我發(fā)現(xiàn)確實(shí)是這樣的,LGStudent 耶動態(tài)生成了一個 NSKVONotifying_LGStudent 子類。

(lldb) po object_getClass(self.person.st)
NSKVONotifying_LGStudent

結(jié)論

經(jīng)過前面這么多分析,KVO 的大致流程和原理我們野梳理的差不多了。

1、動態(tài)注冊子類 NSKVONotifying_XXX。
2、判斷當(dāng)前是否是屬性(因?yàn)樾枰貙憇etter: 方法)。
3、修改當(dāng)前對象isa指針指向動態(tài)子類NSKVONotifying_XXX。
4、調(diào)用setter 方法,并且轉(zhuǎn)發(fā)給父類同時發(fā)出通知通知觀察者observeValueForKeyPath: ofObject: change: context:。
5、在調(diào)用removeObserver:forKeyPath: 后有將isa 指回原來的類。

?著作權(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)容