
搞完KVC搞KVO,誰(shuí)讓他們名字這么接近呢,是吧?KVO其實(shí)我們都很熟悉了,這里就不做過(guò)多的文字描述了,無(wú)非就是給一個(gè)對(duì)象的屬性添加一個(gè)觀察者可以實(shí)現(xiàn)觀察檢測(cè)該屬性值的變化的這么一個(gè)機(jī)制。我們這里就直接進(jìn)入主題去探索下他的一些細(xì)節(jié)和原理。
KVO方法簡(jiǎn)介
我們先來(lái)看看我們經(jīng)常用的KVO的方法:

第一個(gè)參數(shù)是一般為self。第二個(gè)為KeyPath,就是我們要監(jiān)聽(tīng)的key。第三個(gè)option和第四個(gè)context我們看下面官方解釋:
第三個(gè)參數(shù):option

上面官方的解釋其實(shí)就是對(duì)option這個(gè)內(nèi)容選項(xiàng)做了一個(gè)解釋簡(jiǎn)單來(lái)講就是:
NSKeyValueObservingOptionOld : 選擇在更改之前接收觀察屬性的值 也就是觀察舊值。
NSKeyValueObservingOptionNew: 請(qǐng)求屬性的新值。也就是觀察新值變化。
NSKeyValueObservingOptionInitial :發(fā)送立即更改通知(在 addObserver:forKeyPath:options:context:returns 之前)??梢允褂眠@個(gè)額外的一次性通知來(lái)在觀察者中建立屬性的初始值。
NSKeyValueObservingOptionPrior : 指示被觀察對(duì)象在屬性更改之前發(fā)送通知(除了更改之后的通常通知之外)。更改字典通過(guò)包含鍵NSKeyValueChangeNotificationIsPriorKey 和包含YES 的 NSNumber 值來(lái)表示更改前通知。該密鑰不存在。當(dāng)觀察者自己的 KVO 合規(guī)性要求它為依賴于被觀察屬性的屬性之一調(diào)用 -willChange... 方法之一時(shí),您可以使用 prechange 通知。通常的更改后通知來(lái)得太晚了,無(wú)法及時(shí)調(diào)用 。

總結(jié)的話就是監(jiān)聽(tīng)的
Key的不同情況下的值的變化策略。
第四個(gè)參數(shù):context

關(guān)于文中的context,其實(shí)簡(jiǎn)單來(lái)講就是為了使我們觀察的對(duì)象值更加安全更加有針對(duì)性的正確獲取而存在。你可以設(shè)置為Null。你也可以設(shè)置一個(gè)靜態(tài)變量的地址。而且這是蘋(píng)果推薦的方式。因?yàn)樵谖覀冊(cè)谑褂?code>KVO的過(guò)程中,我們可能會(huì)對(duì)多個(gè)對(duì)象多個(gè)屬性進(jìn)行觀察,這時(shí)候我們經(jīng)常用KeyPath和object同時(shí)判斷來(lái)區(qū)分,但是有時(shí)候難免出現(xiàn)重合或者誤寫(xiě)的情況導(dǎo)致獲取的值混亂,并且代碼判斷變多,可讀性變差,復(fù)雜。這個(gè)時(shí)候context就可以發(fā)揮作用了,用它來(lái)區(qū)分每一個(gè)對(duì)象每一個(gè)屬性值的變化。
示例:


context可以更加方便和準(zhǔn)確的一對(duì)一獲取對(duì)象和值的變化。
KVO移除

上面的文章大致講的內(nèi)容就是舉例移除KVO觀察者的方法。同時(shí)下方比較值得注意的就是有講到 如果我們不主動(dòng)移除觀察者,那么當(dāng)我們的key的值發(fā)生變化時(shí)就會(huì)繼續(xù)給觀察者發(fā)消息。這樣就有一種情況出現(xiàn)。當(dāng)我們從頁(yè)面A跳轉(zhuǎn)到 頁(yè)面B 我們給頁(yè)面B 的某個(gè)對(duì)象(非單例對(duì)象)的屬性添加觀察者。并且發(fā)送消息,這個(gè)時(shí)候沒(méi)有問(wèn)題。然后我們從頁(yè)面B返回頁(yè)面A然后再次進(jìn)入頁(yè)面B 并且給新增的觀察者發(fā)送消息的時(shí)候(改變B頁(yè)面被觀察的某個(gè)對(duì)象的屬性)這個(gè)時(shí)候也沒(méi)問(wèn)題,但是當(dāng)我們把B頁(yè)面的對(duì)象換成單例對(duì)象的時(shí)候就會(huì)奔潰。原因就是因?yàn)榍懊娴谝淮芜M(jìn)來(lái)B頁(yè)面創(chuàng)建的對(duì)象觀察者沒(méi)有移除,當(dāng)?shù)诙芜M(jìn)來(lái)的時(shí)候單例對(duì)象還存在只是前一個(gè)B頁(yè)面已經(jīng)釋放了,這個(gè)時(shí)候系統(tǒng)仍然會(huì)給前面釋放掉的B頁(yè)面里未移除的觀察者的發(fā)送消息,但是這個(gè)觀察者的內(nèi)存地址已經(jīng)隨著頁(yè)面B的消失而被移除了。所以當(dāng)我們發(fā)送消息的時(shí)候第一次設(shè)置的觀察者接收消息就報(bào)錯(cuò)了。下面我們把兩種情況都運(yùn)行試試:
情況一:非單例對(duì)象添加觀察者不移除






非單例對(duì)象不移除,不會(huì)造成崩潰。
情況二:?jiǎn)卫龑?duì)象添加觀察者不移除
在情況一上做些改造:




對(duì)單例對(duì)象添加觀察者不移除,當(dāng)持有者(
self)釋放后再次給觀察者發(fā)送消息就會(huì)造成崩潰報(bào)空指針。
KVO自動(dòng)開(kāi)關(guān)控制
1,打開(kāi)自動(dòng)開(kāi)關(guān)(默認(rèn)是打開(kāi)的)


2,關(guān)閉自動(dòng)開(kāi)關(guān)(默認(rèn)是打開(kāi)的)

我們可以利用這個(gè)開(kāi)關(guān)來(lái)控制某個(gè)對(duì)象的觀察者開(kāi)關(guān)選擇
KVO設(shè)置影響因素


可以對(duì)觀察對(duì)象屬性設(shè)置影響因素,改變影響因素即可得到觀察對(duì)象屬性的變化值。
KVO觀察數(shù)組
在KVO文檔開(kāi)頭有告訴我們要了解KVO就要先了解KVC(如圖24)在上一篇文章KVC分析中我們重點(diǎn)分析KVC的細(xì)節(jié)和要點(diǎn),其實(shí)在KVC文檔里有告訴我們關(guān)于KVC和KVO的一些關(guān)聯(lián)(如圖25)。


上面的文檔告訴我們:如果我們?cè)谟?code>KVO來(lái)操作可變的一些集合類型屬性時(shí)就需要按照上面文檔給出的方法來(lái)執(zhí)行。



在上圖我們發(fā)現(xiàn)可變數(shù)組在修改值之后change打印的時(shí)候 kind變成了2。這個(gè)我們?nèi)ゲ榭聪拢?code>command+點(diǎn)擊觀察方法里的NSKeyValueChangeKey:

chang里的kind是一個(gè)枚舉類型,剛好insert是枚舉類型定義的2。
KVO原理探究
我們?cè)?code>KVO的官方文檔詳細(xì)介紹里看到下面一段話:

谷歌翻譯:
自動(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í)際類。
您永遠(yuǎn)不應(yīng)該依賴isa 指針來(lái)確定類成員資格。相反,您應(yīng)該使用類方法來(lái)確定對(duì)象實(shí)例的類。
從上面的文檔我們可以知道KVO在實(shí)現(xiàn)過(guò)程中還生成了中間產(chǎn)物,并且這個(gè)中間產(chǎn)物還把我們觀察對(duì)象的isa指針進(jìn)行了指向修改。
動(dòng)態(tài)生成NSKVONotifying_ZYPerson類
下面我們就來(lái)利用斷點(diǎn)和LLDB調(diào)試打印探索:

在我們
addObserve的時(shí)候動(dòng)態(tài)生成了一個(gè)類:NSKVONotifying_ZYPerson。
我們來(lái)看看這個(gè)新生的NSKVONotifying_ZYPerson類和本類ZYPerson的關(guān)系:
利用以下方法遍歷打印類和子類
#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
// 注冊(cè)類的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建一個(gè)數(shù)組, 其中包含給定對(duì)象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 獲取所有已注冊(cè)的類
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[I]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}

從上圖的打印可知:
NSKVONotifying_ZYPerson類是本類ZYPerson的子類。
既然我們知道了NSKVONotifying_ZYPerson類是動(dòng)態(tài)生成的ZYPerson的子類。那我們就去看看這個(gè)新生成的類內(nèi)容有哪些。比如方法、屬性、協(xié)議等。這里我們探索下方法。
動(dòng)態(tài)生成NSKVONotifying_ZYPerson類的方法
我們利用下面的方法代碼來(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);
}

從上圖我們可以看到 除了方法_isKVOA作為一個(gè)標(biāo)志符號(hào)之外 其他的方法都是其父類ZYPerson擁有的。所以我們可以知道他是在重寫(xiě)父類的方法。
ps:為了方便下面的驗(yàn)證調(diào)試,我們創(chuàng)建一個(gè)新的類ZYViewController。把viewController里的代碼都搬到ZYViewController然后從ViewController頁(yè)面push過(guò)去。
繼續(xù),上面我們看到NSKVONotifying_ZYPerson類繼承了父類ZYPerson的方法。而且我們?cè)谖臋n看到說(shuō)在addObserver后底層進(jìn)行了isa-swizzling操作。將原來(lái)對(duì)象的isa指向了新建的類。那我們就來(lái)驗(yàn)證下:


在添加觀察者的過(guò)程中確實(shí)進(jìn)行了
isa指向轉(zhuǎn)移,從元對(duì)象轉(zhuǎn)移指向了動(dòng)態(tài)創(chuàng)建的NSKVONotifying_xxx類,并且在當(dāng)前頁(yè)面銷毀走dealloc的時(shí)候?qū)⒈挥^察者對(duì)象的isa轉(zhuǎn)移回元對(duì)象本身。
動(dòng)態(tài)生成的子類NSKVONotifying_xxx會(huì)銷毀么
下面我們不禁有疑問(wèn),既然在最后頁(yè)面走dealloc之后會(huì)把isa指針指回,那么動(dòng)態(tài)創(chuàng)建的子類NSKVONotifying_xxx會(huì)被銷毀么?
下面我們來(lái)探究下:


我們通過(guò)
KVO添加觀察者動(dòng)態(tài)生成的子類NSKVONotifying_xxx``并不會(huì)隨著觀察對(duì)象的銷毀而銷毀而是一直存在于原對(duì)象的子類列表中。
重寫(xiě)的setter和class方法
1,class方法重寫(xiě)探索:
我們?cè)谏厦婵梢钥吹絼?dòng)態(tài)生成的NSKVONotifying_ZYPerson子類重寫(xiě)了setter和calss方法,那么我們不妨來(lái)看看 當(dāng)我們給person對(duì)象的nickName屬性添加觀察者后(動(dòng)態(tài)生成子類后),打印下 person的類這個(gè)時(shí)候是什么。

我們發(fā)現(xiàn)打印出來(lái)的還是
ZYPerson類,也就是說(shuō)蘋(píng)果處理這個(gè)子類NSKVONotifying_ZYPerson的時(shí)候 在明面上給開(kāi)發(fā)者看到的還是原本的那個(gè)類。生成的子類只是在后臺(tái)幫我們處理一些事物并不會(huì)顯示出來(lái)。
2,setter方法重寫(xiě)探索:
下面我們來(lái)探索下setter方法到底做了什么。到這里我們不禁思考到一點(diǎn),NSKVONotifying_ZYPerson子類重寫(xiě)setter方法的目的。如果說(shuō)重寫(xiě)setter方法就是為了達(dá)到監(jiān)聽(tīng)的作用那么成員變量是不是就監(jiān)聽(tīng)不到了(屬性才會(huì)自動(dòng)生成setter/getter方法)?


觀察者確實(shí)是針對(duì)
setter方法進(jìn)行的監(jiān)聽(tīng),所以沒(méi)有setter方法的成員變量監(jiān)聽(tīng)不到
到此我們又有了一個(gè)疑問(wèn),KVO確實(shí)是重寫(xiě)并監(jiān)聽(tīng)了setter方法。那么他監(jiān)聽(tīng)的setter方法是自己重寫(xiě)的呢?還是父類的呢?正常來(lái)講應(yīng)該是監(jiān)聽(tīng)自己重寫(xiě)的,不然重寫(xiě)的意義就沒(méi)有了。下面我們看看:

從上圖我們發(fā)現(xiàn)在isa指針指回父類的時(shí)候打印父類的nickName發(fā)現(xiàn)值變化了,而且是我們監(jiān)聽(tīng)的值。這就有點(diǎn)奇怪了。下面我們利用lldb下符號(hào)斷點(diǎn)的方式來(lái)查看下ZYPerson的屬性nickName的變化。下完斷點(diǎn)運(yùn)行點(diǎn)擊頁(yè)面賦值結(jié)果如下圖42

到此我們斷住了ZYPerson的nickName屬性。我們利用bt命令觀察堆棧變化。如圖43

從上圖我們可知在底層其實(shí)是調(diào)用了Foundation框架的一系列的方法:
-[ZYPerson setNickName:]
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
_NSSetObjectValueAndNotify
所以我們可以知道,其實(shí)在底層他并不是直接調(diào)用了setter方法來(lái)賦值的,而是調(diào)用了一系列如:_changeValueForKeys的方法最終實(shí)現(xiàn)setter方法賦值。我們可以利用剛才的斷點(diǎn)查看下這些方法都做了什么,我們直接去看斷點(diǎn)的匯編:




從上面的匯編流程我們看到當(dāng)觀察到值變化后調(diào)用了NSKeyValueWillChange ,然后走到了斷點(diǎn)setter方法,然后就調(diào)用NSKeyValueDidChange。然后發(fā)通知給觀察者。我們進(jìn)一步驗(yàn)證下,我們?cè)谟^察者方法打上斷點(diǎn).


果然,當(dāng)監(jiān)聽(tīng)的setter方法改變時(shí)候,就會(huì)走
NSKeyValueWillChange然后設(shè)置值然后走NSKeyValueDidChange方法,最后發(fā)通知NSKeyValueNotifyObserver。
總結(jié)
KVO流程:
1,我們給對(duì)象屬性設(shè)置觀察者
2,系統(tǒng)自動(dòng)生成對(duì)象的子類NSKVONotifying_xxx,將原對(duì)象的isa指向生成的子類并且自動(dòng)重寫(xiě)class、setter、dealloc等方法。
3,改變觀察對(duì)象子類NSKVONotifying_xxx屬性的值(self.person.nickName = @"WY"; set新值,此時(shí)我們實(shí)際調(diào)用的是動(dòng)態(tài)生成的子類的setter方法而非原類的setter方法)
4,通知父類,調(diào)用父類setter方法修改父類的屬性值
5,通知觀察者持有者,調(diào)用到觀察者observeValueForKeyPath方法。
6,當(dāng)觀察者持有者調(diào)用removeObserver:forKeyPath:釋放觀察者,就會(huì)將isa指回父類。但是此時(shí)動(dòng)態(tài)生成的子類NSKVONotifying_xxx不會(huì)釋放。
至此,文章就算是完結(jié)了,對(duì)于KVO的一些API 和原理都有做了簡(jiǎn)單的分析。下面還有一篇文章我們將會(huì)去嘗試自己自定以一個(gè)KVO。
遇事不決,可問(wèn)春風(fēng)。站在巨人的肩膀上學(xué)習(xí),如有疏忽或者錯(cuò)誤的地方還請(qǐng)多多指教。謝謝!