搞完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)變量的地址
睦刃。而且這是蘋果推薦的方式。因?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)重合
或者誤寫
的情況導(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
擁有的。所以我們可以知道他是在重寫父類的方法驼修。
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ì)象的子類列表中幸乒。
重寫的setter
和class
方法
1懦底,class
方法重寫探索:
我們?cè)谏厦婵梢钥吹絼?dòng)態(tài)生成的NSKVONotifying_ZYPerson
子類重寫了setter
和calss
方法,那么我們不妨來(lái)看看 當(dāng)我們給person對(duì)象
的nickName屬性
添加觀察者后(動(dòng)態(tài)生成子類后)罕扎,打印下 person
的類
這個(gè)時(shí)候是什么聚唐。
我們發(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
方法)?
觀察者確實(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)有了备蚓。下面我們看看:
從上圖我們發(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)重寫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)多多指教砾嫉。謝謝幼苛!