前言
什么是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
從日常的開發(fā)中看出無非就是三個(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;
那么接下來就具體看看這幾個(gè)API到底有何作用。
1尘喝、NSKeyValueObservingOptions 的作用。
NSKeyValueObservingOptionOld
和 NSKeyValueObservingOptionNew
是我們常用的兩個(gè)選選項(xiàng)斋陪。
下面通過一個(gè) demo
來驗(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)聽?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í)候友题。而從官方文檔上來看就是 :
一種更安全嗤堰、更可擴(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)梁棠,這個(gè)context
確實(shí)可以被帶到通知里面去。這樣我們就可以更加好判斷誰監(jiān)聽的誰斗埂。也可以保證在移除觀察者的時(shí)候不會(huì)出現(xiàn)問題(不會(huì)把父類相同的監(jiān)聽給移除了)符糊。
// 這樣,即使父類也有一個(gè)觀察了name 的觀察者呛凶,只要context 不一樣男娄,就不會(huì)隨意的移除掉。
[self.person removeObserver:self forKeyPath:@"name" context:personName]
3、要不要移除觀察者
通常來說模闲,我們注冊(cè)的觀察者一旦執(zhí)行了 dealloc
以后建瘫,那么被觀察的對(duì)象也就釋放了。所以移除與否都沒有關(guān)系尸折。但是有一些情況是啰脚,雖然我的觀察者釋放了,但是這個(gè)被觀察的對(duì)象依然還存在实夹,那這個(gè)時(shí)候在給這個(gè)觀察者發(fā)生通知那就會(huì)出問題了橄浓。比如我們上面的被觀察的對(duì)象是個(gè)單列,或者其他一些暫時(shí)沒辦法釋放的東西亮航,那么下次在給當(dāng)前對(duì)象發(fā)生通知就會(huì)觸發(fā)野指針而崩潰荸实。
所以,最好還是在我們觀察者 dealloc
的時(shí)候缴淋,執(zhí)行 remove
准给。
4、手動(dòng)和自動(dòng)監(jiān)聽KVO
在api
里面還有一個(gè) +automaticallyNotifiesObserversForKey
:方法重抖,這個(gè)方法默認(rèn)返回 true
露氮。也就是默認(rèn)開啟自動(dòng)發(fā)送通知,如果我們返回 false
那么久沒發(fā)自動(dòng)發(fā)送通知仇哆,需要手動(dòng)發(fā)送通知沦辙,即調(diào)用 willChangeValueForKey:
and didChangeValueForKey:
者兩個(gè)方法來手動(dòng)發(fā)出通知。也可以通過 + (BOOL)automaticallyNotifiesObserversOfName
這個(gè)方法來指定某個(gè)屬性是和否可以自動(dòng)發(fā)出通知(這個(gè)要在automaticallyNotifiesObserversForKey:
沒有重寫的情況下)讹剔。
// 自動(dòng)開關(guān)關(guān)閉
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return false;
}
當(dāng)我們重寫了如上的方法后油讯,整個(gè)類的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)聽集合類型
如果我們要監(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ù)組的成員是不會(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í)行集合類型的觀察在配合 options
可以看看是什么效果,閣下可以自己去嘗試看看結(jié)果是如何的狞玛。筆者這里就不在細(xì)說软驰,還有包括KVC
的相關(guān)的一些對(duì)應(yīng)的情況,可以查閱筆者關(guān)于KVC 的表述
6心肪、監(jiān)聽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)的锭亏,接下來我們就去探索。這里借助LLDB
和 api
來一起驗(yàn)證硬鞍。
1慧瘤、探索isa
Automatic key-value observing is implemented using a technique called isa-swizzling.
從官方文檔來看戴已,自動(dòng)KVO
是一種isa-swizzling
,那么我們就先來看看這個(gè)isa
到底是什么锅减,如下實(shí)現(xiàn)一段代碼糖儡,并且下一個(gè)斷點(diǎn),分別在添加觀察者和添加后打印結(jié)果
查看isa
從結(jié)果我們可以看出怔匣,在添加了觀察者后握联,isa
指向了一個(gè) 名為 NSKVONotifying_LGPerson
的類。那么這個(gè)類和我們的 LGPerson
有什么關(guān)系呢劫狠?那么結(jié)合我們前面類的原理里面探索的拴疤,類結(jié)構(gòu)的第二個(gè)成員變量是 superClass
,可以得出他們是父子關(guān)系独泞。
(lldb) po 0x00000001c28f8628
NSObject
(lldb) po 0x0000000104a55650
LGPerson
7、NSKVONotifying_CDPerson 里面有什么東西<成員變量苔埋、方法懦砂、協(xié)議>
這里筆者采用api來看看當(dāng)前這個(gè)類里面到底有什么。
接下來調(diào)用如下一個(gè)方法來探索這個(gè)類里面有什么成員组橄。
- (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)聽后分別查看這個(gè)類的相關(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è)問題是設(shè)個(gè) st.name 到底是在何處監(jiān)聽的荞膘?
從結(jié)果我們可以看到,并沒有setsSt.name 這樣的方法玉工。只有一個(gè) setSt:的方法羽资,這就讓我懷疑是不是 LGStudent 也有創(chuàng)建了一個(gè)動(dòng)態(tài)了的類,而這種多級(jí)監(jiān)聽最后只是通過kvc 傳遞到了里面相關(guān)的對(duì)象里面去了遵班。
通過調(diào)試我發(fā)現(xiàn)確實(shí)是這樣的屠升,LGStudent 耶動(dòng)態(tài)生成了一個(gè) NSKVONotifying_LGStudent 子類。
(lldb) po object_getClass(self.person.st)
NSKVONotifying_LGStudent
結(jié)論
經(jīng)過前面這么多分析狭郑,KVO 的大致流程和原理我們野梳理的差不多了腹暖。
1、動(dòng)態(tài)注冊(cè)子類 NSKVONotifying_XXX翰萨。
2脏答、判斷當(dāng)前是否是屬性(因?yàn)樾枰貙憇etter: 方法)。
3亩鬼、修改當(dāng)前對(duì)象isa指針指向動(dòng)態(tài)子類NSKVONotifying_XXX殖告。
4、調(diào)用setter 方法雳锋,并且轉(zhuǎn)發(fā)給父類同時(shí)發(fā)出通知通知觀察者observeValueForKeyPath: ofObject: change: context:黄绩。
5、在調(diào)用removeObserver:forKeyPath: 后有將isa 指回原來的類魄缚。