KVO 分析

1.jpeg

搞完KVCKVO,誰(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的方法:

2.png

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

第三個(gè)參數(shù):option
3.png

上面官方的解釋其實(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 和包含YESNSNumber 值來(lái)表示更改前通知。該密鑰不存在。當(dāng)觀察者自己的 KVO 合規(guī)性要求它為依賴于被觀察屬性的屬性之一調(diào)用 -willChange... 方法之一時(shí),您可以使用 prechange 通知。通常的更改后通知來(lái)得太晚了,無(wú)法及時(shí)調(diào)用 。

5.png

總結(jié)的話就是監(jiān)聽(tīng)的Key的不同情況下的值的變化策略。

第四個(gè)參數(shù):context
4.png

關(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)常用KeyPathobject同時(shí)判斷來(lái)區(qū)分,但是有時(shí)候難免出現(xiàn)重合或者誤寫(xiě)的情況導(dǎo)致獲取的值混亂,并且代碼判斷變多,可讀性變差,復(fù)雜。這個(gè)時(shí)候context就可以發(fā)揮作用了,用它來(lái)區(qū)分每一個(gè)對(duì)象每一個(gè)屬性值的變化。

示例:

6.png
7.png

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

KVO移除

8.png

上面的文章大致講的內(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ì)象添加觀察者不移除
9.png
10.png
11.png
12.png
13.png
14.png

非單例對(duì)象不移除,不會(huì)造成崩潰。

情況二:?jiǎn)卫龑?duì)象添加觀察者不移除

在情況一上做些改造:

15.png
16.png
17.png
18.png

對(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)的)
19.png
20.png
2,關(guān)閉自動(dòng)開(kāi)關(guān)(默認(rèn)是打開(kāi)的)
21.png

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

KVO設(shè)置影響因素

22.png
23.png

可以對(duì)觀察對(duì)象屬性設(shè)置影響因素,改變影響因素即可得到觀察對(duì)象屬性的變化值。

KVO觀察數(shù)組

KVO文檔開(kāi)頭有告訴我們要了解KVO就要先了解KVC(如圖24)在上一篇文章KVC分析中我們重點(diǎn)分析KVC的細(xì)節(jié)和要點(diǎn),其實(shí)在KVC文檔里有告訴我們關(guān)于KVCKVO的一些關(guān)聯(lián)(如圖25)。

24.png
25.png

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

26.png
27.png
28.png

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

29.png

chang里的kind是一個(gè)枚舉類型,剛好insert是枚舉類型定義的2。

KVO原理探究

我們?cè)?code>KVO的官方文檔詳細(xì)介紹里看到下面一段話:

30.png

谷歌翻譯:
自動(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)試打印探索:

31.png

在我們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);
}
32.png

從上圖的打印可知: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);
}
33.png

從上圖我們可以看到 除了方法_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)證下:

34.png
35.png

在添加觀察者的過(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)探究下:

36.png
37.png

我們通過(guò)KVO添加觀察者動(dòng)態(tài)生成的子類NSKVONotifying_xxx``并不會(huì)隨著觀察對(duì)象的銷毀銷毀而是一直存在于原對(duì)象的子類列表中。

重寫(xiě)的setterclass方法
1,class方法重寫(xiě)探索:

我們?cè)谏厦婵梢钥吹絼?dòng)態(tài)生成的NSKVONotifying_ZYPerson子類重寫(xiě)了settercalss方法,那么我們不妨來(lái)看看 當(dāng)我們給person對(duì)象nickName屬性添加觀察者后(動(dòng)態(tài)生成子類后),打印下 person這個(gè)時(shí)候是什么。

38.png

我們發(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方法)?

39.png
40.png

觀察者確實(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)有了。下面我們看看:

41.png

從上圖我們發(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

42.png

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

43.png

從上圖我們可知在底層其實(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)的匯編:

44.png
45.png
46.png
47.png

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

48.png
49.png

果然,當(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)多多指教。謝謝!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容