1.概述
很多童鞋在iOS開(kāi)發(fā)中都聽(tīng)過(guò)所謂的KVO,其實(shí)這只是一個(gè)縮寫(xiě),也只是一種開(kāi)發(fā)模式,它的全稱是Key-Value Observing (觀察者模式) 是蘋(píng)果Fundation框架下提供的一種開(kāi)發(fā)機(jī)制,使用KVO,可以方便地對(duì)指定對(duì)象的某個(gè)屬性進(jìn)行觀察,當(dāng)屬性發(fā)生變化時(shí),進(jìn)行通知,告訴開(kāi)發(fā)者屬性舊值和新值對(duì)應(yīng)的內(nèi)容這種開(kāi)發(fā)模式在實(shí)際開(kāi)發(fā)中應(yīng)用場(chǎng)景還是很多的,同時(shí)也是很多大企業(yè)在面試過(guò)程中必不可少的面試題之一,也是考察中高級(jí)程序員的核心標(biāo)準(zhǔn),所以,搞清楚KVO的原理和使用方法也是所有iOS程序員的必備的技能和核心基礎(chǔ)。
2.原理
要了解KVO的機(jī)制我們必須對(duì)KVO底層實(shí)現(xiàn)要有一定的掌握。蘋(píng)果 使用了 isa 混寫(xiě)(isa-swizzling)來(lái)實(shí)現(xiàn) KVO 。也就是說(shuō)當(dāng)觀察對(duì)象A時(shí),KVO機(jī)制動(dòng)態(tài)創(chuàng)建一個(gè)新的名為:NSKVONotifying_A 的新類,該類繼承自對(duì)象A的本類,且 KVO 為 NSKVONotifying_A 重寫(xiě)觀察屬性的 setter 方法,setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前和之后,通知所有觀察對(duì)象屬性值的更改情況。
(備注: isa 混寫(xiě)(isa-swizzling)isa:is a kind of ; swizzling:混合,攪合;)
說(shuō)起isa指針,我們可以用下面的兩個(gè)圖來(lái)解釋:

上圖我們可以看到,一個(gè)實(shí)例變量的isa指針是指向它的類對(duì)象的,然而一個(gè)類對(duì)象的isa指針是指向其元類對(duì)象。一層一層的遞進(jìn)。而KVO在被創(chuàng)建的時(shí)候則會(huì)自動(dòng)產(chǎn)生一個(gè)NSKVONotifying_A 的新類,而實(shí)例對(duì)象的isa則會(huì)指向這個(gè)新類,新類的isa指針則繼續(xù)指向這個(gè)實(shí)例對(duì)象的類對(duì)象,從而監(jiān)聽(tīng)這個(gè)類對(duì)象屬性的set方法,用兩張圖來(lái)打個(gè)比方,假如我們新建一個(gè)Person對(duì)象,如果沒(méi)有使用KVO應(yīng)該是這樣的:


這里我使用了兩張MJ老師制作的PPT來(lái)引用我上述的說(shuō)法,MJ老師做的圖相當(dāng)?shù)那宄椭庇^,我們可以看到,在普通沒(méi)有使用KVO時(shí),我們創(chuàng)建一個(gè)Person類之后Person的實(shí)例對(duì)象的isa指針是直接指向其類對(duì)象的,然后由類對(duì)象來(lái)處理屬性和屬性對(duì)應(yīng)的setter方法,但是第二個(gè)圖我們就可以看到,如果使用了KVO之后,那系統(tǒng)就會(huì)通過(guò)Runtime機(jī)制動(dòng)態(tài)創(chuàng)建一個(gè)NSKVONotifying_A的對(duì)象,而Person的isa指針就會(huì)指向這個(gè)類對(duì)象,由這個(gè)對(duì)象去監(jiān)聽(tīng)Person的類對(duì)象。
NSKVONotifying_A子類setter方法剖析
KVO 的鍵值觀察通知依賴于 NSObject 的兩個(gè)方法:willChangeValueForKey:和 didChangevlueForKey:,在存取數(shù)值的前后分別調(diào)用 2 個(gè)方法:被觀察屬性發(fā)生改變之前,willChangeValueForKey:被調(diào)用,通知系統(tǒng)該 keyPath 的屬性值即將變更;當(dāng)改變發(fā)生后, didChangeValueForKey: 被調(diào)用,通知系統(tǒng)該 keyPath 的屬性值已經(jīng)變更;之后, observeValueForKey:ofObject:change:context: 也會(huì)被調(diào)用。且重寫(xiě)觀察屬性的 setter 方法這種繼承方式的注入是在運(yùn)行時(shí)而不是編譯時(shí)實(shí)現(xiàn)的。KVO 為子類的觀察者屬性重寫(xiě)調(diào)用存取方法的工作原理在代碼中相當(dāng)于:
-(void)setName:(NSString *)newName{
[self willChangeValueForKey:@"name"]; //KVO 在調(diào)用存取方法之前總調(diào)用
[super setValue:newName forKey:@"name"]; //調(diào)用父類的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在調(diào)用存取方法之后總調(diào)用
}
ps:注意點(diǎn):
觀察者觀察的是屬性,只有遵循 KVO 變更屬性值的方式才會(huì)執(zhí)行 KVO 的回調(diào)方法,例如是否執(zhí)行了 setter 方法、或者是否使用了 KVC 賦值。
如果賦值沒(méi)有通過(guò) setter 方法或者 KVC,而是直接修改屬性對(duì)應(yīng)的成員變量,例如:僅調(diào)用 _name = @"newName",這時(shí)是不會(huì)觸發(fā) KVO 機(jī)制,更加不會(huì)調(diào)用回調(diào)方法的。
所以使用 KVO 機(jī)制的前提是遵循 KVO 的屬性設(shè)置方式來(lái)變更屬性值。
3.如何使用
那么在接觸完KVO的原理和概念之后我們就要了解如何去使用KVO了,其實(shí)KVO的使用并不難,我們這就來(lái)上代碼:
1. 注冊(cè)O(shè)bserver:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
即:
addObserver:forKeyPath:options:context:
上述的參數(shù)含義:
observer:觀察者,需要響應(yīng)屬性變化的對(duì)象。該對(duì)象必須實(shí)現(xiàn) observeValueForKeyPath:ofObject:change:context: 方法。
keyPath:要觀察的屬性名稱。要和屬性聲明的名稱一致。
options:對(duì)KVO機(jī)制進(jìn)行配置,修改KVO通知的時(shí)機(jī)以及通知的內(nèi)容。
context:context是一個(gè)c指針,可以傳入任意類型的對(duì)象,在觀察者接收通知回調(diào)的方法 observeValueForKeyPath:ofObject:change:context: 中可以接收到這個(gè)對(duì)象,是KVO中的一種傳值方式。這個(gè)參數(shù)可以用來(lái)區(qū)分同一對(duì)象對(duì)同一個(gè)屬性的多個(gè)不同的監(jiān)聽(tīng)。
這里有兩點(diǎn)是需要注意的:
observeValueForKeyPath:ofObject:change:context:方法的對(duì)象是目標(biāo)對(duì)象,observer是觀察者對(duì)象,keyPath是目標(biāo)對(duì)象的屬性。
注意,在注冊(cè)了Observer后,一定要在合適時(shí)機(jī)移除注冊(cè),否則會(huì)crash。移除注冊(cè)的兩種方法:
- (void)removeObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context
蘋(píng)果官方推薦的方式是,在init的時(shí)候進(jìn)行addObserver,在dealloc時(shí)removeObserver,這樣可以保證add和remove是成對(duì)出現(xiàn)的,是一種比較理想的使用方式。
第二種方法帶有context屬性,主要是用來(lái)區(qū)分不同的觀察者Observer的。
如果observer沒(méi)有監(jiān)聽(tīng)keyPath屬性,則調(diào)用這兩個(gè)方法會(huì)拋出異常并崩潰。所以,必須確保先注冊(cè)了觀察者,才能調(diào)用移除方法。。實(shí)際上,在添加觀察者的時(shí)候,觀察者對(duì)象與被觀察屬性所屬的對(duì)象都不會(huì)被retain,然而在這些對(duì)象被釋放后,相關(guān)的監(jiān)聽(tīng)信息卻還存在,KVO做的處理是直接讓程序崩潰。
- options參數(shù)是一個(gè)枚舉類型,共有四種取值方式:
enum {
NSKeyValueObservingOptionNew = 0x01, //新值
NSKeyValueObservingOptionOld = 0x02, //舊值
NSKeyValueObservingOptionInitial = 0x04, //
NSKeyValueObservingOptionPrior = 0x08
};
- NSKeyValueObservingOptionNew:接收方法中使用change參數(shù)傳入變化后的新值,鍵為:NSKeyValueChangeNewKey;
- NSKeyValueObservingOptionOld:接收方法中使用change參數(shù)傳入變化前的舊值,鍵為:NSKeyValueChangeOldKey;
- NSKeyValueObservingOptionInitial:注冊(cè)之后立即調(diào)用一次接收方法。如果還如果配置了NSKeyValueObservingOptionNew,change參數(shù)內(nèi)容會(huì)包含新值,鍵為:NSKeyValueChangeNewKey。
- NSKeyValueObservingOptionPrior:如果加入這個(gè)參數(shù),接收方法會(huì)在變化前后分別調(diào)用一次,共兩次,變化前的通知change參數(shù)包含notificationIsPrior = 1。其他內(nèi)容根據(jù)NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld的配置確定。
注意:options參數(shù)可以配置多個(gè),如:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld,使用 | 或運(yùn)算符連接。
調(diào)用addObserver:forKeyPath:options:context:方法時(shí),觀察者對(duì)象與被觀察屬性所屬的對(duì)象都不會(huì)被retain,也就是說(shuō),引用計(jì)數(shù)不會(huì)加1。
可以重復(fù)添加監(jiān)聽(tīng):可以多次調(diào)用addObserver:..方法,將同一對(duì)象注冊(cè)為同一屬性的的觀察者(參數(shù)可以完全相同,可以使用context參數(shù)進(jìn)行區(qū)分)。這些觀察者會(huì)并存。
2.接收通知
當(dāng)被監(jiān)聽(tīng)的屬性的值發(fā)生變化時(shí),KVO會(huì)自動(dòng)通知注冊(cè)了的觀察者。上文提到,觀察者必須實(shí)現(xiàn)以下方法,這個(gè)方法就是觀察者接收通知的方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
參數(shù):
object:目標(biāo)對(duì)象,即所監(jiān)聽(tīng)的對(duì)象,也就是所監(jiān)聽(tīng)的屬性所屬的對(duì)象。
change:是傳入的變化量,通過(guò)在注冊(cè)時(shí)用options參數(shù)進(jìn)行的配置,會(huì)包含不同的內(nèi)容。
- change參數(shù)
除了根據(jù)options參數(shù)控制的change參數(shù)內(nèi)容,默認(rèn)change參數(shù)會(huì)包含一個(gè)NSKeyValueChangeKindKey鍵值對(duì),傳遞被監(jiān)聽(tīng)屬性的變化類型:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
- NSKeyValueChangeSetting:屬性的值被重新設(shè)置;
- NSKeyValueChangeInsertion、NSKeyValueChangeRemoval NSKeyValueChangeReplacement:表示更改的是集合屬性,分別代表插入、刪除、替換操作。
- 如果NSKeyValueChangeKindKey參數(shù)是針對(duì)集合屬性的三個(gè)之一,change參數(shù)還會(huì)包含一個(gè)NSKeyValueChangeIndexesKey鍵值對(duì),表示變化的index。
- change字典里,新值的key為“new”,舊值的key為“old”,變化類型的key為“kind”。
3. 示例
-(void)setKVO{
_p = [[Person alloc]init];
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
//點(diǎn)擊改變對(duì)象的名字
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_p.name = @"leon";
}
//接收改變的新舊值
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
// NSKeyValueChangeNewKey == @"new"
NSString *new = change[NSKeyValueChangeNewKey];
// NSKeyValueChangeOldKey == @"old"
NSString *old = change[NSKeyValueChangeOldKey];
NSLog(@"%@-%@",new,old);
}
本文參考鏈接:
iOS 關(guān)于KVO的一些總結(jié)
iOS開(kāi)發(fā) -- KVO的實(shí)現(xiàn)原理與具體應(yīng)用