
前言
什么是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ī)制,它允許對(duì)象在其他對(duì)象的指定屬性發(fā)生更改時(shí)收到通知。
KVO基礎(chǔ)
KVO 從日常的開(kāi)發(fā)中看出無(wú)非就是三個(gè)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;
那么接下來(lái)就具體看看這幾個(gè)API到底有何作用。
1、NSKeyValueObservingOptions 的作用。
NSKeyValueObservingOptionOld 和 NSKeyValueObservingOptionNew 是我們常用的兩個(gè)選選項(xiàng)。
下面通過(guò)一個(gè) demo 來(lái)驗(yàn)證這個(gè)到底有什么作用
先準(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:@"+"];
}
這時(shí)候我們分別監(jiān)聽(tīng)?zhēng)讉€(gè)不同的 options ,可以得到如下的結(jié)果
- NSKeyValueObservingOptionOld
change = {
kind = 1;
old = Hello;
}
- NSKeyValueObservingOptionNew
change = {
kind = 1;
new = "Hello+";
}
- NSKeyValueObservingOptionPrior
change = {
kind = 1;
notificationIsPrior = 1;
}
change = {
kind = 1;
}
2、 context
上下文。這種設(shè)計(jì)在很多場(chǎng)景都有實(shí)用,特別是在CF、CG等框架的時(shí)候。而從官方文檔上來(lái)看就是 :
一種更安全、更可擴(kuò)展的方法是使用上下文來(lái)確保您收到的通知是發(fā)送給您的觀察者而不是超類(lèi)的。
那么我們來(lái)驗(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-";
}
通過(guò)結(jié)果我們發(fā)現(xiàn),這個(gè)context 確實(shí)可以被帶到通知里面去。這樣我們就可以更加好判斷誰(shuí)監(jiān)聽(tīng)的誰(shuí)。也可以保證在移除觀察者的時(shí)候不會(huì)出現(xiàn)問(wèn)題(不會(huì)把父類(lèi)相同的監(jiān)聽(tīng)給移除了)。
// 這樣,即使父類(lèi)也有一個(gè)觀察了name 的觀察者,只要context 不一樣,就不會(huì)隨意的移除掉。
[self.person removeObserver:self forKeyPath:@"name" context:personName]
3、要不要移除觀察者
通常來(lái)說(shuō),我們注冊(cè)的觀察者一旦執(zhí)行了 dealloc 以后,那么被觀察的對(duì)象也就釋放了。所以移除與否都沒(méi)有關(guān)系。但是有一些情況是,雖然我的觀察者釋放了,但是這個(gè)被觀察的對(duì)象依然還存在,那這個(gè)時(shí)候在給這個(gè)觀察者發(fā)生通知那就會(huì)出問(wèn)題了。比如我們上面的被觀察的對(duì)象是個(gè)單列,或者其他一些暫時(shí)沒(méi)辦法釋放的東西,那么下次在給當(dāng)前對(duì)象發(fā)生通知就會(huì)觸發(fā)野指針而崩潰。
所以,最好還是在我們觀察者 dealloc 的時(shí)候,執(zhí)行 remove。
4、手動(dòng)和自動(dòng)監(jiān)聽(tīng)KVO
在api 里面還有一個(gè) +automaticallyNotifiesObserversForKey:方法,這個(gè)方法默認(rèn)返回 true。也就是默認(rèn)開(kāi)啟自動(dòng)發(fā)送通知,如果我們返回 false 那么久沒(méi)發(fā)自動(dòng)發(fā)送通知,需要手動(dòng)發(fā)送通知,即調(diào)用 willChangeValueForKey:and didChangeValueForKey: 者兩個(gè)方法來(lái)手動(dòng)發(fā)出通知。也可以通過(guò) + (BOOL)automaticallyNotifiesObserversOfName 這個(gè)方法來(lái)指定某個(gè)屬性是和否可以自動(dòng)發(fā)出通知(這個(gè)要在automaticallyNotifiesObserversForKey:沒(méi)有重寫(xiě)的情況下)。
// 自動(dòng)開(kāi)關(guān)關(guān)閉
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return false;
}
當(dāng)我們重寫(xiě)了如上的方法后,整個(gè)類(lèi)的KVO 就不會(huì)自動(dòng)觸發(fā)通知的發(fā)送。這個(gè)時(shí)候就需要手動(dòng)去觸發(fā):
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
5、監(jiān)聽(tīng)集合類(lèi)型
如果我們要監(jiān)聽(tīng)集合類(lèi)型的屬性(如:NSArray),那么我們實(shí)現(xiàn)如下監(jiān)聽(tīng)。
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"array" options:(NSKeyValueObservingOptionNew) context:NULL];
如果直接改變數(shù)組的成員是不會(huì)觸發(fā)的,只有按照KVC 的方式去觸發(fā)才可以觸發(fā)通知的發(fā)送。
/// 這樣是不會(huì)生效的
[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í)行集合類(lèi)型的觀察在配合 options 可以看看是什么效果,閣下可以自己去嘗試看看結(jié)果是如何的。筆者這里就不在細(xì)說(shuō),還有包括KVC 的相關(guān)的一些對(duì)應(yīng)的情況,可以查閱筆者關(guān)于KVC 的表述
6、監(jiān)聽(tīng)keyPath 多級(jí)路徑
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)的,接下來(lái)我們就去探索。這里借助LLDB 和 api 來(lái)一起驗(yàn)證。
1、探索isa
Automatic key-value observing is implemented using a technique called isa-swizzling.
從官方文檔來(lái)看,自動(dòng)KVO是一種isa-swizzling,那么我們就先來(lái)看看這個(gè)isa到底是什么,如下實(shí)現(xiàn)一段代碼,并且下一個(gè)斷點(diǎn),分別在添加觀察者和添加后打印結(jié)果
查看isa
從結(jié)果我們可以看出,在添加了觀察者后,isa指向了一個(gè) 名為 NSKVONotifying_LGPerson 的類(lèi)。那么這個(gè)類(lèi)和我們的 LGPerson 有什么關(guān)系呢?那么結(jié)合我們前面類(lèi)的原理里面探索的,類(lèi)結(jié)構(gòu)的第二個(gè)成員變量是 superClass ,可以得出他們是父子關(guān)系。
(lldb) po 0x00000001c28f8628
NSObject
(lldb) po 0x0000000104a55650
LGPerson
7、NSKVONotifying_CDPerson 里面有什么東西<成員變量、方法、協(xié)議>
這里筆者采用api來(lái)看看當(dāng)前這個(gè)類(lèi)里面到底有什么。
接下來(lái)調(diào)用如下一個(gè)方法來(lái)探索這個(gè)類(lèi)里面有什么成員。
- (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)聽(tīng)前后監(jiān)聽(tīng)后分別查看這個(gè)類(lèi)的相關(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)];
這里筆者有個(gè)問(wèn)題是設(shè)個(gè) st.name 到底是在何處監(jiān)聽(tīng)的?


從結(jié)果我們可以看到,并沒(méi)有setsSt.name 這樣的方法。只有一個(gè) setSt:的方法,這就讓我懷疑是不是 LGStudent 也有創(chuàng)建了一個(gè)動(dòng)態(tài)了的類(lèi),而這種多級(jí)監(jiān)聽(tīng)最后只是通過(guò)kvc 傳遞到了里面相關(guān)的對(duì)象里面去了。
通過(guò)調(diào)試我發(fā)現(xiàn)確實(shí)是這樣的,LGStudent 耶動(dòng)態(tài)生成了一個(gè) NSKVONotifying_LGStudent 子類(lèi)。
(lldb) po object_getClass(self.person.st)
NSKVONotifying_LGStudent
結(jié)論
經(jīng)過(guò)前面這么多分析,KVO 的大致流程和原理我們野梳理的差不多了。
1、動(dòng)態(tài)注冊(cè)子類(lèi) NSKVONotifying_XXX。
2、判斷當(dāng)前是否是屬性(因?yàn)樾枰貙?xiě)setter: 方法)。
3、修改當(dāng)前對(duì)象isa指針指向動(dòng)態(tài)子類(lèi)NSKVONotifying_XXX。
4、調(diào)用setter 方法,并且轉(zhuǎn)發(fā)給父類(lèi)同時(shí)發(fā)出通知通知觀察者observeValueForKeyPath: ofObject: change: context:。
5、在調(diào)用removeObserver:forKeyPath: 后有將isa 指回原來(lái)的類(lèi)。
