前言
上一篇文章學(xué)習(xí)了KVC的原理(鍵值編碼),KVC是由NSKeyValueCoding非正式協(xié)議啟用的一種機(jī)制,對(duì)象采用該機(jī)制來(lái)提供對(duì)其屬性的間接訪問(wèn)。而KVO的實(shí)現(xiàn)是基于KVC鍵值編碼,以下我們進(jìn)行探討。
準(zhǔn)備工作
KVO協(xié)議定義
KVO全稱是Key-value Observing,翻譯過(guò)來(lái)就是:鍵值觀察。提供了一種當(dāng)其它對(duì)象屬性被修改的時(shí)候能通知當(dāng)前對(duì)象的機(jī)制。
-
KVO的定義
類似于KVC,KVO的定義都是對(duì)NSObject的擴(kuò)展來(lái)實(shí)現(xiàn)的,KVO的定義在Foundation里面,而Foundation框架是不開源的,只能在蘋果官方文檔查找。見下圖:
KVO分類定義
KVO(鍵值觀察)是一種機(jī)制,它允許對(duì)象在其他對(duì)象的指定屬性發(fā)生更改時(shí)收到通知。它對(duì)于應(yīng)用程序中模型層和控制器層之間的通信特別有用。
注意:要使用KVO,首先必須確保被觀察對(duì)象符合KVO。一般情況下,如果您的對(duì)象繼承自NSObject并且您以通常的方式創(chuàng)建屬性,則您的對(duì)象及其屬性將自動(dòng)符合KVO。
KVO提供的API- 監(jiān)聽注冊(cè)
使用方法addObserver:forKeyPath:options:context:向被觀察對(duì)象注冊(cè)觀察者。必須執(zhí)行以下步驟才能使對(duì)象能夠接收KVO兼容屬性的鍵值觀察通知,觀察者指定一個(gè)選項(xiàng)參數(shù)options和一個(gè)上下文指針context來(lái)管理通知的各個(gè)方面。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- 接收通知
在觀察者內(nèi)部實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:以接受更改通知消息。
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- 移除監(jiān)聽
使用方法- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;移除觀察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
KVO的使用
監(jiān)聽選項(xiàng)option
監(jiān)聽選項(xiàng)是由枚舉NSKeyValueObservingOptions定義的:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
}
注意:option會(huì)影響通知中,提供的更改字典的內(nèi)容以及生成通知的方式
-
NSKeyValueObservingOptionNew
監(jiān)聽獲取屬性的新值,見下圖:
監(jiān)聽獲取屬性新值 -
NSKeyValueObservingOptionOld
監(jiān)聽屬性獲取舊值,見下圖:
監(jiān)聽屬性獲取舊值 -
NSKeyValueObservingOptionInitial
添加觀察者的時(shí)候立即發(fā)送一個(gè)通知給觀察者,見下圖:
發(fā)送通知給觀察者
在每次修改屬性時(shí),會(huì)在修改通知被發(fā)送之前預(yù)先發(fā)送一條通知給觀察者,這與-willChangeValueForKey:被觸發(fā)的時(shí)間是相對(duì)應(yīng)的。這樣,在每次修改屬性時(shí),實(shí)際上是會(huì)發(fā)送兩條通知,見下圖:
修改屬性發(fā)送兩條通知
上下文指針Context
addObserver:forKeyPath:options:context:消息中的上下文指針包含任意數(shù)據(jù),這些數(shù)據(jù)將在相應(yīng)的更改通知中傳遞回觀察者。您可以指定NULL并完全依賴鍵路徑字符串來(lái)確定更改通知的來(lái)源,但這種方法可能會(huì)導(dǎo)致超類由于不同原因也在觀察相同鍵路徑的對(duì)象出現(xiàn)問(wèn)題。
一下實(shí)現(xiàn)一個(gè)案例,LGStudent繼承自LGPerson,同時(shí)對(duì)兩個(gè)對(duì)象的name屬行進(jìn)行設(shè)置,通過(guò)添加上下文指針context,可以在接收通知的地方進(jìn)行過(guò)濾。見下圖:

KVO使用技巧
-
同一個(gè)對(duì)象重復(fù)注冊(cè)為同一屬性的觀察者
可以多次調(diào)用addObserver:forKeyPath:options:context:這個(gè)方法,將同一個(gè)對(duì)象注冊(cè)為同一屬性的觀察者(所有參數(shù)可以完全相同)。這時(shí),即便在所有參數(shù)一致的情況下,新注冊(cè)的觀察者并不會(huì)替換原來(lái)觀察者,而是會(huì)并存。這樣,當(dāng)屬性被修改時(shí),兩次監(jiān)聽都會(huì)響應(yīng)。見下面的案例分析:
案例分析
通過(guò)以上的案例,KVO注冊(cè)多少次就會(huì)有多少次的回調(diào),不會(huì)覆蓋相同的觀察者。 移除觀察者
在觀察者不再需要監(jiān)聽屬性變化時(shí),必須調(diào)用removeObserver:forKeyPath:或removeObserver:forKeyPath:context:方法來(lái)移除觀察者,這兩個(gè)方法的聲明如下:
- (void)removeObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context
這兩個(gè)方法會(huì)根據(jù)傳入的參數(shù)(主要是keyPath和context)來(lái)移除觀察者。移除觀察者可以避免監(jiān)聽回調(diào)的混亂,保持良好的代碼質(zhì)量。
注意:如果observer沒有監(jiān)聽keyPath屬性,依然調(diào)用上面兩個(gè)方法會(huì)拋出異常,見下圖:

由以上案例可知,
觀察者的移除是必須確認(rèn)觀察者已經(jīng)被注冊(cè)了,這樣子才能調(diào)用移除觀察者的方法,如果我們沒有移除觀察者也會(huì)出現(xiàn)崩潰的情況,請(qǐng)往下看。添加觀察時(shí),兩個(gè)對(duì)象(即觀察者對(duì)象及屬性所屬的對(duì)象)都不會(huì)被
retain,然而在這些對(duì)象被釋放后,相關(guān)的監(jiān)聽信息卻還存在,KVO做的處理是直接讓程序崩潰。其實(shí)蘋果官網(wǎng)也給出了相關(guān)說(shuō)明,見下圖:
- 如果尚未注冊(cè)為觀察者,則要求將其移除為觀察者會(huì)導(dǎo)致
NSRangeException。
- 如果尚未注冊(cè)為觀察者,則要求將其移除為觀察者會(huì)導(dǎo)致
- 解除分配時(shí),觀察者
不會(huì)自動(dòng)刪除自己。被觀察的對(duì)象繼續(xù)發(fā)送通知,而忽略了觀察者的狀態(tài)。但是,發(fā)送到已釋放對(duì)象的更改通知與任何其他消息一樣,會(huì)觸發(fā)內(nèi)存訪問(wèn)異常。因此,您要確保觀察者在從記憶中消失之前將自己移除。
- 解除分配時(shí),觀察者
- 該協(xié)議沒有提供詢問(wèn)對(duì)象是觀察者還是被觀察者的方法。所以在構(gòu)建代碼時(shí),避免與發(fā)布相關(guān)的錯(cuò)誤。
自動(dòng)監(jiān)聽和手動(dòng)監(jiān)聽
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
默認(rèn)情況下,該方法返回YES,即表示默認(rèn)可以對(duì)任何類中的所有屬性進(jìn)行監(jiān)聽,可以理解為自動(dòng)監(jiān)聽。在這種模式下,當(dāng)我們修改屬性的值時(shí),KVO會(huì)自動(dòng)調(diào)用以下兩個(gè)方法:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
開發(fā)過(guò)程中,可能不需要對(duì)所有屬性進(jìn)行監(jiān)聽,只要求選擇性的觀察部分屬性。此時(shí)+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法返回NO,那么就需要對(duì)屬性進(jìn)行手動(dòng)監(jiān)聽。見下面代碼:
// 自動(dòng)監(jiān)聽開關(guān)-關(guān)閉
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
此時(shí)自動(dòng)監(jiān)聽開關(guān)已經(jīng)關(guān)閉,如果需要監(jiān)聽person對(duì)象的name屬性的變化,就需要在setter方法中添加willChangeValueForKey和didChangeValueForKey方法,兩個(gè)方法必須成對(duì)出現(xiàn),否則無(wú)效。
如果我們?cè)陂_發(fā)過(guò)程中只是針對(duì)某幾個(gè)屬性進(jìn)行手動(dòng)接收通知,其他的不需要手動(dòng)接收通知,那么我們可以精確的做到這個(gè)動(dòng)作,通過(guò)+automaticallyNotifiesObserversForKey:方法可以設(shè)置對(duì)象中哪些屬性需要手動(dòng)處理,那么可以自動(dòng)處理。見下圖:

-
確保屬性發(fā)生變化發(fā)送通知
如果希望只有當(dāng)屬性值實(shí)際被修改時(shí)發(fā)送通知,以盡量減少不必要的通知,則可以如下實(shí)現(xiàn):(做屬性的攔截判斷)
- (void)setNick:(NSString *)nick{
if (nick != _nick){
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
}
補(bǔ)充:如果我們?cè)?code>setter方法之外改變了實(shí)例變量(如_nick),且希望這種修改被觀察者監(jiān)聽到,則需要像在setter方法里面做一樣的處理。這也涉及到我們通常會(huì)遇到的一個(gè)問(wèn)題,在類的內(nèi)部,對(duì)于一個(gè)屬性值,何時(shí)用屬性(self.nick)訪問(wèn),而何時(shí)用實(shí)例變量(_nick)訪問(wèn)。一般的建議是,在獲取屬性值時(shí),可以用實(shí)例變量;在設(shè)置屬性值時(shí),盡量用setter方法,以保證屬性的KVO特性。當(dāng)然,性能也是一個(gè)考量,在設(shè)置值時(shí),使用實(shí)例變量比使用屬性設(shè)置值的性能高不少。
-
多屬性依賴
我們監(jiān)聽的某個(gè)屬性可能會(huì)依賴于其它多個(gè)屬性的變化,不管所依賴的哪個(gè)屬性發(fā)生了變化,都會(huì)導(dǎo)致計(jì)算屬性的變化。對(duì)于這種一對(duì)一(To-one)的關(guān)系,我們需要做兩步操作,首先是確定計(jì)算屬性與所依賴屬性的關(guān)系。如我們?cè)?code>Person類中定義一個(gè)fullName屬性,其getter方法定義如下:
- (NSString *)fullName {
return [[NSString alloc] initWithFormat:@"he's full name is :%@ , %@", self.name, self.nick];
}
定義了這種依賴關(guān)系后,需要以某種方式告訴KVO,當(dāng)我們的被依賴屬性修改時(shí),會(huì)發(fā)送fullName屬性被修改的通知。此時(shí),我們需要重寫NSKeyValueObserving協(xié)議的keyPathsForValuesAffectingValueForKey:方法,這個(gè)方法返回的是一個(gè)集合對(duì)象,包含了哪些影響key指定的屬性依賴的屬性所對(duì)應(yīng)的字符串。所以對(duì)于fullName屬性,該方法的實(shí)現(xiàn)如下:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"name", @"nick"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
看看fullName監(jiān)聽效果:

-
集合屬性的監(jiān)聽
對(duì)于不可變集合屬性,我們更多的是把它當(dāng)成一個(gè)整體來(lái)監(jiān)聽,而無(wú)法去監(jiān)聽集合中的某個(gè)元素的變化;對(duì)于可變集合屬性,實(shí)際上也是當(dāng)成一個(gè)整體,去監(jiān)聽它整體的變化,如添加、刪除和替換元素。具體案例如下:
可變數(shù)組添加監(jiān)聽
如果想監(jiān)聽集合中數(shù)據(jù)的變化,如添加、刪除和替換元素該如何處理呢?向可變數(shù)組中添加元素,這種處理方式沒有效果。見下圖:
數(shù)組中添加元素案例
KVO鍵值監(jiān)聽實(shí)現(xiàn)的基礎(chǔ)是KVC。我們以數(shù)組為例,在我們的Person類中有一個(gè)dateArray數(shù)組屬性,如果我們希望響應(yīng)dateArray所有的方法,則需要實(shí)現(xiàn)以下方法:
官方說(shuō)明
所以對(duì)于可變集合,我們不使用valueForKey:來(lái)獲取對(duì)象,而是使用以下方法:
案例分析
由打印信息可以發(fā)現(xiàn)kind字段的值發(fā)生了而變化,輸出值為2、3、4。這是因?yàn)椋?code>KVO機(jī)制能在集合改變的時(shí)候把詳細(xì)的變化放進(jìn)change字典中。
補(bǔ)充:集合(Set)也有一套對(duì)應(yīng)的方法來(lái)實(shí)現(xiàn)集合代理對(duì)象,包括無(wú)序集合與有序集合;而字典則沒有,對(duì)于字典屬性的監(jiān)聽,還是只能作為一個(gè)整體來(lái)處理。
如果我們想到手動(dòng)控制集合屬性消息的發(fā)送,則可以使用上面提到的幾個(gè)方法,即:
-willChange:valuesAtIndexes:forKey:
-didChange:valuesAtIndexes:forKey:
或
-willChangeValueForKey:withSetMutation:usingObjects:
-didChangeValueForKey:withSetMutation:usingObjects:
注意:先要把自動(dòng)通知關(guān)閉,否則每次改變KVO都會(huì)被發(fā)送兩次。
-
變化字典
觀察者對(duì)象必須實(shí)現(xiàn)-observeValueForKeyPath:ofObject:change:context:方法,來(lái)對(duì)屬性修改通知做相應(yīng)的處理。這個(gè)方法的聲明如下:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
第三個(gè)參數(shù),通常稱之為變化字典(Change Dictionary),它記錄了被監(jiān)聽屬性的變化情況。這個(gè)字典中包含的值,會(huì)根據(jù)我們?cè)谔砑佑^察者時(shí)設(shè)置的options參數(shù)的不同而有所不同,它包含了屬性被修改的一些信息。我們可以通過(guò)以下key來(lái)獲取我們想要的信息:
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/*
Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
其中,NSKeyValueChangeKindKey的值取自于NSKeyValueChange,它的值是由以下枚舉定義的:
enum {
// 設(shè)置一個(gè)新值。被監(jiān)聽的屬性可以是一個(gè)對(duì)象,也可以是一對(duì)一關(guān)系的屬性或一對(duì)多關(guān)系的屬性。
NSKeyValueChangeSetting = 1,
// 表示一個(gè)對(duì)象被插入到一對(duì)多關(guān)系的屬性。
NSKeyValueChangeInsertion = 2,
// 表示一個(gè)對(duì)象被從一對(duì)多關(guān)系的屬性中移除。
NSKeyValueChangeRemoval = 3,
// 表示一個(gè)對(duì)象在一對(duì)多的關(guān)系的屬性中被替換
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
KVO實(shí)現(xiàn)原理
在上面了解了NSKeyValueObserving所提供的功能后,我們?cè)賮?lái)看看KVO的實(shí)現(xiàn)機(jī)制,以便更深入地的理解KVO。KVO沒有開源,所以我們無(wú)法從源代碼的層面來(lái)分析它的實(shí)現(xiàn)。那么我們還是先查看官方的描述:

翻譯過(guò)來(lái):自動(dòng)鍵值觀察是使用一種稱為
isa-swizzling的技術(shù)實(shí)現(xiàn)的。isa指針指向維護(hù)調(diào)度表的對(duì)象的類。 該調(diào)度表主要包含指向類實(shí)現(xiàn)的方法的指針,以及其他數(shù)據(jù)。當(dāng)觀察者為對(duì)象的屬性注冊(cè)時(shí),被觀察對(duì)象的isa指針被修改,指向中間類而不是真正的類。 因此,isa指針的值不一定反映實(shí)例的實(shí)際類。所以我們就提出了幾個(gè)疑問(wèn),這個(gè)
isa指向的中間類是什么?kvo觀察的是setter方法,setter方法做了什么,調(diào)用的又是誰(shuí)的setter方法?移除監(jiān)聽后這個(gè)中間類是否銷毀呢?帶著疑問(wèn)我們繼續(xù)往下走。
-
尋找中間類
NSKVONotifying_LGPerson
首先我們通過(guò)設(shè)置斷點(diǎn),來(lái)逐步跟蹤person對(duì)象isa指針?biāo)赶虻念?,見下圖:
跟蹤isa指向
在添加監(jiān)聽之前,person對(duì)象對(duì)應(yīng)的類是LGPerson,添加過(guò)監(jiān)聽之后,person對(duì)象isa指向的類是NSKVONotifying_LGPerson。這個(gè)類應(yīng)該就是官網(wǎng)中說(shuō)到的中間類。
那么這個(gè)中間類是何時(shí)創(chuàng)建的呢?我們?cè)谡{(diào)用addObserver:forKeyPath:options:context:方法之前,獲取NSKVONotifying_LGPerson這個(gè)類,發(fā)現(xiàn)這個(gè)類并不存在。見下圖:

說(shuō)明這個(gè)類應(yīng)該是通過(guò)
runtime在運(yùn)行時(shí)動(dòng)態(tài)生成的。
-
NSKVONotifying_LGPerson和LGPerson的關(guān)系
通過(guò)lldb調(diào)試,打印NSKVONotifying_LGPerson類的地址,獲取其內(nèi)存空間,發(fā)現(xiàn)NSKVONotifying_LGPerson的父類是LGPerson類。
案例分析
所以,NSKVONotifying_LGPerson是LGPerson的子類。(如果不明白為什么看內(nèi)存就知道是父類的話建議區(qū)看看類的內(nèi)存結(jié)構(gòu)) 中間類提供的方法
提供下面一個(gè)輔助方法,用來(lái)獲取類中的方法列表。如下:
#pragma mark **- 遍歷方法-ivar-property**
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
在調(diào)用addObserver:forKeyPath:options:context:方法之后,調(diào)用該輔助方法,查看NSKVONotifying_LGPerson類中有哪些功能。見下圖:

發(fā)現(xiàn)中間類
重寫了父類的四個(gè)方法。分別是setNickName、class、dealloc、_isKVOA。
對(duì)象的
isa何時(shí)修復(fù)
通過(guò)上面的分析,我們發(fā)現(xiàn)在調(diào)用addObserver:forKeyPath:options:context:方法之后,對(duì)象的isa指向了一個(gè)中間類,那么isa和在重新執(zhí)行LGPerson類呢?-
(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;方法,也就是移除監(jiān)聽的時(shí)候。我們來(lái)驗(yàn)證一下:
驗(yàn)證isa修復(fù)
在調(diào)用- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;方法之后,對(duì)象的isa指針重新指向了LGPerson類。
注意:在完成觀察者的銷毀之后,這個(gè)中間類依然存在,并沒有被銷毀。(為下次使用做準(zhǔn)備,性能的考慮,避免重復(fù)創(chuàng)建),請(qǐng)繼續(xù)往下看。

-
中間類中
setter方法的作用
這里setter方法做了什么,監(jiān)聽的是屬性還是成員變量呢?我們做個(gè)監(jiān)聽分別采用操作屬性和訪問(wèn)成員變量的方式,分別變更nickName和name,見下圖:
驗(yàn)證中間類的setter方法
說(shuō)明KVO實(shí)際是通過(guò)setter方法監(jiān)聽的是屬性。我們可以通過(guò)監(jiān)聽nickName成員變量來(lái)分析底層調(diào)用過(guò)程。見下圖:
堆棧分析
通過(guò)堆棧我們可以發(fā)現(xiàn),在調(diào)用setNickName方法是,底層實(shí)際是調(diào)用了下面的流程: Foundation _NSSetObjectValueAndNotifyFoundation -[NSObject(NSKeyValueObservingPrivate)_changeValueForKey:key:key:usingBlock:]Foundation -[NSObject(NSKeyValueObservingPrivate)_changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
總結(jié)
Objective-C基于強(qiáng)大的run time機(jī)制來(lái)實(shí)現(xiàn)KVO。當(dāng)?shù)谝淮斡^察某個(gè)對(duì)象的屬性時(shí),run time會(huì)創(chuàng)建一個(gè)新的繼承自這個(gè)對(duì)象的class的subclass。在這個(gè)新的subclass中,它會(huì)重寫所有被觀察的key的setter方法,然后將對(duì)象的isa指針指向新創(chuàng)建的class(這個(gè)指針告訴Objective-C運(yùn)行時(shí)某個(gè)對(duì)象到底是什么類型的)。所以實(shí)例對(duì)象變成了新的子類的實(shí)例。完成以上操作后,通過(guò)調(diào)用setter方法進(jìn)行相關(guān)屬性的變化時(shí),操作的就是這個(gè)中間的子類。但是底層依然會(huì)將對(duì)中間類操作的狀態(tài),同步到原對(duì)象中。在進(jìn)行監(jiān)聽移除后,對(duì)象的isa回復(fù)到原來(lái)的類上,且中間類沒有跟著被移除。














