什么是KVO?
KVO
全稱(chēng)Key Value Observing
,是蘋(píng)果提供的一套事件通知機(jī)制舰褪。允許對(duì)象監(jiān)聽(tīng)另一個(gè)對(duì)象特定屬性的改變蜂大,并在改變時(shí)接收到事件闽铐。由于KVO
的實(shí)現(xiàn)機(jī)制,只針對(duì)屬性才會(huì)發(fā)生作用奶浦,一般繼承自NSObject
的對(duì)象都默認(rèn)支持KVO
兄墅。
KVO
可以監(jiān)聽(tīng)單個(gè)屬性的變化,也可以監(jiān)聽(tīng)集合對(duì)象的變化财喳。通過(guò)KVC
的mutableArrayValueForKey:
等方法獲得代理對(duì)象察迟,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)斩狱,會(huì)回調(diào)KVO
監(jiān)聽(tīng)的方法。集合對(duì)象包含NSArray
和NSSet
扎瓶。
KVO基本使用
- 使用KVO大致分為三個(gè)步驟:
- 通過(guò)
addObserver:forKeyPath:options:context:
方法注冊(cè)觀(guān)察者所踊,觀(guān)察者可以接收keyPath
屬性的變化事件 - 在觀(guān)察者中實(shí)現(xiàn)
observeValueForKeyPath:ofObject:change:context:
方法,當(dāng)keyPath
屬性發(fā)生改變后概荷,KVO
會(huì)回調(diào)這個(gè)方法來(lái)通知觀(guān)察者 - 當(dāng)觀(guān)察者不需要監(jiān)聽(tīng)時(shí)秕岛,可以調(diào)用
removeObserver:forKeyPath:
方法將KVO
移除。需要注意的是误证,調(diào)用removeObserver
需要在觀(guān)察者消失之前继薛,否則會(huì)導(dǎo)致Crash
- 通過(guò)
注冊(cè)觀(guān)察者
/*
@observer:就是觀(guān)察者,是誰(shuí)想要觀(guān)測(cè)對(duì)象的值的改變愈捅。
@keyPath:就是想要觀(guān)察的對(duì)象屬性遏考。
@options:options一般選擇NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,這樣當(dāng)屬性值發(fā)生改變時(shí)我們可以同時(shí)獲得舊值和新值蓝谨,如果我們只填NSKeyValueObservingOptionNew則屬性發(fā)生改變時(shí)只會(huì)獲得新值灌具。
@context:想要攜帶的其他信息,比如一個(gè)字符串或者字典什么的譬巫。
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
監(jiān)聽(tīng)回調(diào)
/*
@keyPath:觀(guān)察的屬性
@object:觀(guān)察的是哪個(gè)對(duì)象的屬性
@change:這是一個(gè)字典類(lèi)型的值咖楣,通過(guò)鍵值對(duì)顯示新的屬性值和舊的屬性值
@context:上面添加觀(guān)察者時(shí)攜帶的信息
*/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
調(diào)用方式
自動(dòng)調(diào)用
- 調(diào)用KVO屬性對(duì)象時(shí),不僅可以通過(guò)點(diǎn)語(yǔ)法和set語(yǔ)法進(jìn)行調(diào)用芦昔,還可以使用KVC方法
//通過(guò)屬性的點(diǎn)語(yǔ)法間接調(diào)用
objc.name = @"";
// 直接調(diào)用set方法
[objc setName:@"Savings"];
// 使用KVC的setValue:forKey:方法
[objc setValue:@"Savings" forKey:@"name"];
// 使用KVC的setValue:forKeyPath:方法
[objc setValue:@"Savings" forKeyPath:@"account.name"];
手動(dòng)調(diào)用
-
KVO 在屬性發(fā)生改變時(shí)的調(diào)用是自動(dòng)的诱贿,如果想要手動(dòng)控制這個(gè)調(diào)用時(shí)機(jī),或想自己實(shí)現(xiàn) KVO 屬性的調(diào)用咕缎,則可以通過(guò) KVO 提供的方法進(jìn)行調(diào)用珠十。
- 第一步我們需要認(rèn)識(shí)下面這個(gè)方法,如果想要手動(dòng)調(diào)用或自己實(shí)現(xiàn)KVO需要重寫(xiě)該方法該方法返回YES表示可以調(diào)用锨阿,返回NO則表示不可以調(diào)用宵睦。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"name"]) { automatic = NO;//對(duì)該key禁用系統(tǒng)自動(dòng)通知,若要直接禁用該類(lèi)的KVO則直接返回NO墅诡; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; }
- 第二步我們需要重寫(xiě)setter方法
- (void)setName:(NSString *)name { if (name != _name) { [self willChangeValueForKey:@"name"]; _name = name; [self didChangeValueForKey:@"name"]; } }
移除觀(guān)察者
//需要在不使用的時(shí)候,移除監(jiān)聽(tīng)
- (void)dealloc{
[self removeObserver:self forKeyPath:@"age"];
}
Crash
觀(guān)察者未實(shí)現(xiàn)監(jiān)聽(tīng)方法
-
若觀(guān)察者對(duì)象 -observeValueForKeyPath:ofObject:change:context: 未實(shí)現(xiàn)壳嚎,將會(huì) Crash
Crash:Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘<ViewController: 0x7f9943d06710>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled
未及時(shí)移除觀(guān)察者
Crash: Thread 1: EXC_BAD_ACCESS (code=1, address=0x105e0fee02c0)
//觀(guān)察者ObserverPersonChage
@interface ObserverPersonChage : NSObject
//實(shí)現(xiàn)observeValueForKeyPath: ofObject: change: context:
@end
//ViewController
- (void)addObserver
{
self.observerPersonChange = [[ObserverPersonChage alloc] init];
[self.person1 addObserver:self.observerPersonChange forKeyPath:@"age" options:option context:@"age chage"];
[self.person1 addObserver:self.observerPersonChange forKeyPath:@"name" options:option context:@"name change"];
}
//點(diǎn)擊按鈕將觀(guān)察者置為nil,即銷(xiāo)毀
- (IBAction)clearObserverPersonChange:(id)sender {
self.observerPersonChange = nil;
}
//點(diǎn)擊改變person1屬性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 29;
self.person1.name = @"hengcong";
}
- 假如在當(dāng)前 ViewController 中末早,注冊(cè)了觀(guān)察者烟馅,點(diǎn)擊屏幕,改變被觀(guān)察對(duì)象 person1 的屬性值然磷。
- 點(diǎn)擊對(duì)應(yīng)按鈕郑趁,銷(xiāo)毀觀(guān)察者,此時(shí) self.observerPersonChange 為 nil姿搜。
- 再次點(diǎn)擊屏幕寡润,此時(shí) Crash捆憎;
多次移除觀(guān)察者
Cannot remove an observer for the key path “age” from because it is not registered as an observer.
實(shí)際應(yīng)用
KVO
主要用來(lái)做鍵值觀(guān)察操作,想要一個(gè)值發(fā)生改變后通知另一個(gè)對(duì)象梭纹,則用KVO
實(shí)現(xiàn)最為合適躲惰。斯坦福大學(xué)的iOS
教程中有一個(gè)很經(jīng)典的案例,通過(guò)KVO
在Model
和Controller
之間進(jìn)行通信变抽。
KVO實(shí)現(xiàn)原理
KVO
是通過(guò)isa 混寫(xiě)(isa-swizzling
)技術(shù)實(shí)現(xiàn)的(是不是一臉懵逼础拨?我第一次見(jiàn)和你一樣,你現(xiàn)在只需要知道這個(gè)技術(shù)就行了绍载,下面我會(huì)圖文并茂的給你講解到底是怎么回事诡宗。)。在運(yùn)行時(shí)根據(jù)原類(lèi)創(chuàng)建一個(gè)中間類(lèi)击儡,這個(gè)中間類(lèi)是原類(lèi)的子類(lèi)塔沃,并動(dòng)態(tài)修改當(dāng)前對(duì)象的isa
指向中間類(lèi)。并且將class
方法重寫(xiě)阳谍,返回原類(lèi)的Class
芳悲。所以蘋(píng)果建議在開(kāi)發(fā)中不應(yīng)該依賴(lài)isa
指針,而是通過(guò)class
實(shí)例方法來(lái)獲取對(duì)象類(lèi)型边坤。
測(cè)試代碼
NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
NSLog(@"person1添加KVO監(jiān)聽(tīng)對(duì)象之前-類(lèi)對(duì)象 -%@", object_getClass(self.person1));
NSLog(@"person1添加KVO監(jiān)聽(tīng)之前-方法實(shí)現(xiàn) -%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person1添加KVO監(jiān)聽(tīng)之前-元類(lèi)對(duì)象 -%@", object_getClass(object_getClass(self.person1)));
[self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
NSLog(@"person1添加KVO監(jiān)聽(tīng)對(duì)象之后-類(lèi)對(duì)象 -%@", object_getClass(self.person1));
NSLog(@"person1添加KVO監(jiān)聽(tīng)之后-方法實(shí)現(xiàn) -%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person1添加KVO監(jiān)聽(tīng)之后-元類(lèi)對(duì)象 -%@", object_getClass(object_getClass(self.person1)));
//打印結(jié)果
KVO-test[1214:513029] person1添加KVO監(jiān)聽(tīng)對(duì)象之前-類(lèi)對(duì)象 -Person
KVO-test[1214:513029] person1添加KVO監(jiān)聽(tīng)之前-方法實(shí)現(xiàn) -0x100411470
KVO-test[1214:513029] person1添加KVO監(jiān)聽(tīng)之前-元類(lèi)對(duì)象 -Person
KVO-test[1214:513029] person1添加KVO監(jiān)聽(tīng)對(duì)象之后-類(lèi)對(duì)象 -NSKVONotifying_Person
KVO-test[1214:513029] person1添加KVO監(jiān)聽(tīng)之后-方法實(shí)現(xiàn) -0x10076c844
KVO-test[1214:513029] person1添加KVO監(jiān)聽(tīng)之后-元類(lèi)對(duì)象 -NSKVONotifying_Person
//通過(guò)地址查找方法
(lldb) p (IMP)0x10f24b470
(IMP) $0 = 0x000000010f24b470 (KVO-test`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x10f5a6844
(IMP) $1 = 0x000000010f5a6844 (Foundation`_NSSetLongLongValueAndNotify)
- 通過(guò)測(cè)試代碼,我們添加KVO前后發(fā)生以下變化
-
person
指向的類(lèi)對(duì)象
和元類(lèi)對(duì)象
谅年,以及setAge:
均發(fā)生了變化茧痒; - 添加KVO后,
person
中的isa
指向了 NSKVONotifying_Person 類(lèi)對(duì)象融蹂; - 添加 KVO 之后旺订,
setAge:
的實(shí)現(xiàn)調(diào)用的是:Foundation 中_NSSetLongLongValueAndNotify
方法;
-
發(fā)現(xiàn)中間對(duì)象
從上述測(cè)試代碼的結(jié)果我們發(fā)現(xiàn)超燃,
person
中的isa
從開(kāi)始指向Person
類(lèi)對(duì)象区拳,變成指向了 NSKVONotifying_Person 類(lèi)對(duì)象
-
KVO
會(huì)在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建一個(gè)新類(lèi),將對(duì)象的isa
指向新創(chuàng)建的類(lèi)意乓,新類(lèi)是原類(lèi)的子類(lèi)樱调,命名規(guī)則是NSKVONotifying_xxx
的格式。- 未使用KVO監(jiān)聽(tīng)對(duì)象是届良,對(duì)象和類(lèi)對(duì)象之間的關(guān)系如下
- 使用KVO監(jiān)聽(tīng)對(duì)象后笆凌,對(duì)象和類(lèi)對(duì)象之間會(huì)添加一個(gè)中間對(duì)象
NSKVONotifying_Person類(lèi)內(nèi)部實(shí)現(xiàn)
我們從上面兩張圖很清楚的看到添加KVO之前和KVO之后的變化,下面我們剖析一下這個(gè)中間類(lèi)NSKVONotifying_Person
(這里是*
通配符,它代表數(shù)據(jù)類(lèi)型士葫,例如:int乞而, longlong)
- (void)setAge:(int)age{
_NSSet*ValueAndNotify();//這個(gè)方法調(diào)用順序是什么,它是在調(diào)用何處方法慢显,都在setter方法改變中詳解
}
- (Class)class {
return [LDPerson class];
}
- (void)dealloc {
// 收尾工作
}
- (BOOL)_isKVOA {
return YES;
}
- isa混寫(xiě)之后如何調(diào)用方法
- 調(diào)用監(jiān)聽(tīng)的屬性設(shè)置方法爪模,如
setAge:
欠啤,都會(huì)先調(diào)用NSKVONotify_Person
對(duì)應(yīng)的屬性設(shè)置方法; - 調(diào)用非監(jiān)聽(tīng)屬性設(shè)置方法屋灌,如
test
洁段,會(huì)通過(guò)NSKVONotify_Person
的superclass
,找到Person
類(lèi)對(duì)象声滥,再調(diào)用其[Person test]
方法
- 調(diào)用監(jiān)聽(tīng)的屬性設(shè)置方法爪模,如
- 為什么重寫(xiě)
class
方法- 如果沒(méi)有重寫(xiě)
class
方法,當(dāng)該對(duì)象調(diào)用class
方法時(shí),會(huì)在自己的方法緩存列表,方法列表,父類(lèi)緩存,方法列表一直向上去查找該方法,因?yàn)?code>class方法是NSObject
中的方法,如果不重寫(xiě)最終可能會(huì)返回NSKVONotifying_Person
,就會(huì)將該類(lèi)暴露出來(lái),也給開(kāi)發(fā)者造成困擾,寫(xiě)的是Person
,添加KVO之后class
方法返回怎么是另一個(gè)類(lèi)眉撵。
- 如果沒(méi)有重寫(xiě)
- _isKVOA有什么作用
- 這個(gè)方法可以當(dāng)做使用了
KVO
的一個(gè)標(biāo)記,系統(tǒng)可能也是這么用的落塑。如果我們想判斷當(dāng)前類(lèi)是否是KVO
動(dòng)態(tài)生成的類(lèi)纽疟,就可以從方法列表中搜索這個(gè)方法。
- 這個(gè)方法可以當(dāng)做使用了
setter實(shí)現(xiàn)不同
在測(cè)試代碼中憾赁,我們已經(jīng)通過(guò)地址查找添加KVO前后調(diào)用的方法
-
//通過(guò)地址查找方法 //添加KVO之前 (lldb) p (IMP)0x10f24b470 (IMP) $0 = 0x000000010f24b470 (KVO-test`-[Person setAge:] at Person.h:15) //添加KVO之后 (lldb) p (IMP)0x10f5a6844 (IMP) $1 = 0x000000010f5a6844 (Foundation`_NSSetLongLongValueAndNotify)
-
0x10f24b470
這個(gè)地址的setAge:
實(shí)現(xiàn)是調(diào)用Person類(lèi)的setAge:
方法污朽,并且是在Person.h的第15行。 - 而
0x10f5a6844
這個(gè)地址的setAge:
實(shí)現(xiàn)是調(diào)用_NSSetIntValueAndNotify
這樣一個(gè)C函數(shù)龙考。
-
KVO內(nèi)部調(diào)用流程
-
由于我們無(wú)法去窺探
_NSSetIntValueAndNotify
的真實(shí)結(jié)構(gòu)蟆肆,也無(wú)法去重寫(xiě)NSKVONotifying_Person
這個(gè)類(lèi),所以我們只能利用它的父類(lèi)Person類(lèi)來(lái)分析其執(zhí)行過(guò)程晦款。- (void)setAge:(int)age{ _age = age; NSLog(@"setAge:"); } - (void)willChangeValueForKey:(NSString *)key{ [super willChangeValueForKey:key]; NSLog(@"willChangeValueForKey"); } - (void)didChangeValueForKey:(NSString *)key{ NSLog(@"didChangeValueForKey - begin"); [super didChangeValueForKey:key]; NSLog(@"didChangeValueForKey - end"); } @end //打印結(jié)果 KVO-test[1457:637227] willChangeValueForKey KVO-test[1457:637227] setAge: KVO-test[1457:637227] didChangeValueForKey - begin KVO-test[1457:637227] didChangeValueForKey - end KVO-test[1457:637227] willChangeValueForKey KVO-test[1457:637227] didChangeValueForKey - begin KVO-test[1457:637227] didChangeValueForKey - end
- 通過(guò)打印結(jié)果炎功,我們可以清晰看到
- 首先調(diào)用
willChangeValueForKey:
方法。 - 然后調(diào)用
setAge:
方法真正的改變屬性的值缓溅。 - 開(kāi)始調(diào)用
didChangeValueForKey:
這個(gè)方法蛇损,調(diào)用[super didChangeValueForKey:key]
時(shí)會(huì)通知監(jiān)聽(tīng)者屬性值已經(jīng)改變,然后監(jiān)聽(tīng)者執(zhí)行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
這個(gè)方法坛怪。
- 首先調(diào)用
- 通過(guò)打印結(jié)果炎功,我們可以清晰看到
-
下面我用一張圖來(lái)展示KVO執(zhí)行流程
KVO擴(kuò)展
1.KVC 與 KVO 的不同淤齐?
- KVC(鍵值編碼),即 Key-Value Coding袜匿,一個(gè)非正式的 Protocol更啄,使用字符串(鍵)訪(fǎng)問(wèn)一個(gè)對(duì)象實(shí)例變量的機(jī)制。而不是通過(guò)調(diào)用 Setter居灯、Getter 方法等顯式的存取方式去訪(fǎng)問(wèn)祭务。
- KVO(鍵值監(jiān)聽(tīng)),即 Key-Value Observing怪嫌,它提供一種機(jī)制,當(dāng)指定的對(duì)象的屬性被修改后,對(duì)象就會(huì)接受到通知待牵,前提是執(zhí)行了 setter 方法、或者使用了 KVC 賦值喇勋。
2.和 notification(通知)的區(qū)別缨该?
-
KVO
和NSNotificationCenter
都是iOS
中觀(guān)察者模式的一種實(shí)現(xiàn)。區(qū)別在于川背,相對(duì)于被觀(guān)察者和觀(guān)察者之間的關(guān)系贰拿,KVO
是一對(duì)一的蛤袒,而不是一對(duì)多的。KVO
對(duì)被監(jiān)聽(tīng)對(duì)象無(wú)侵入性膨更,不需要修改其內(nèi)部代碼即可實(shí)現(xiàn)監(jiān)聽(tīng)妙真。 - notification 的優(yōu)點(diǎn)是監(jiān)聽(tīng)不局限于屬性的變化,還可以對(duì)多種多樣的狀態(tài)變化進(jìn)行監(jiān)聽(tīng)荚守,監(jiān)聽(tīng)范圍廣珍德,例如鍵盤(pán)、前后臺(tái)等系統(tǒng)通知的使用也更顯靈活方便矗漾。