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)變量的地址睦刃。而且這是蘋果推薦的方式。因?yàn)樵谖覀冊(cè)谑褂?code>KVO的過(guò)程中十酣,我們可能會(huì)對(duì)多個(gè)對(duì)象多個(gè)屬性進(jìn)行觀察涩拙,這時(shí)候我們經(jīng)常用KeyPathobject同時(shí)判斷來(lái)區(qū)分,但是有時(shí)候難免出現(xiàn)重合或者誤寫的情況導(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擁有的。所以我們可以知道他是在重寫父類的方法驼修。

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ì)象的子類列表中幸乒。

重寫的setterclass方法
1懦底,class方法重寫探索:

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

38.png

我們發(fā)現(xiàn)打印出來(lái)的還是ZYPerson 類,也就是說(shuō)蘋果處理這個(gè)子類NSKVONotifying_ZYPerson的時(shí)候 在明面上給開(kāi)發(fā)者看到的還是原本的那個(gè)類腔召。生成的子類只是在后臺(tái)幫我們處理一些事物并不會(huì)顯示出來(lái)杆查。

2,setter方法重寫探索:

下面我們來(lái)探索下setter方法到底做了什么臀蛛。到這里我們不禁思考到一點(diǎn)亲桦,NSKVONotifying_ZYPerson子類重寫setter方法的目的崖蜜。如果說(shuō)重寫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í)是重寫并監(jiān)聽(tīng)了setter方法。那么他監(jiān)聽(tīng)的setter方法是自己重寫的呢舔琅?還是父類的呢等恐?正常來(lái)講應(yīng)該是監(jiān)聽(tīng)自己重寫的,不然重寫的意義就沒(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)重寫classsetter腥光、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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市焕刮,隨后出現(xiàn)的幾起案子舶沿,更是在濱河造成了極大的恐慌,老刑警劉巖配并,帶你破解...
    沈念sama閱讀 221,331評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件括荡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡溉旋,警方通過(guò)查閱死者的電腦和手機(jī)畸冲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,372評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)观腊,“玉大人邑闲,你說(shuō)我怎么就攤上這事∷∧” “怎么了监憎?”我有些...
    開(kāi)封第一講書人閱讀 167,755評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)婶溯。 經(jīng)常有香客問(wèn)我,道長(zhǎng)偷霉,這世上最難降的妖魔是什么迄委? 我笑而不...
    開(kāi)封第一講書人閱讀 59,528評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮类少,結(jié)果婚禮上叙身,老公的妹妹穿的比我還像新娘。我一直安慰自己硫狞,他們只是感情好信轿,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,526評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布晃痴。 她就那樣靜靜地躺著,像睡著了一般财忽。 火紅的嫁衣襯著肌膚如雪倘核。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,166評(píng)論 1 308
  • 那天即彪,我揣著相機(jī)與錄音紧唱,去河邊找鬼。 笑死隶校,一個(gè)胖子當(dāng)著我的面吹牛漏益,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播深胳,決...
    沈念sama閱讀 40,768評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼绰疤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了舞终?” 一聲冷哼從身側(cè)響起轻庆,我...
    開(kāi)封第一講書人閱讀 39,664評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎权埠,沒(méi)想到半個(gè)月后榨了,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,205評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡攘蔽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,290評(píng)論 3 340
  • 正文 我和宋清朗相戀三年龙屉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片满俗。...
    茶點(diǎn)故事閱讀 40,435評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡转捕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唆垃,到底是詐尸還是另有隱情五芝,我是刑警寧澤,帶...
    沈念sama閱讀 36,126評(píng)論 5 349
  • 正文 年R本政府宣布辕万,位于F島的核電站枢步,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏渐尿。R本人自食惡果不足惜醉途,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,804評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砖茸。 院中可真熱鬧隘擎,春花似錦、人聲如沸凉夯。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,276評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至震桶,卻和暖如春休傍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尼夺。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工尊残, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人淤堵。 一個(gè)月前我還...
    沈念sama閱讀 48,818評(píng)論 3 376
  • 正文 我出身青樓寝衫,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親拐邪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子慰毅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,442評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容