一、什么是 KVO
首先讓我們了解一下什么KVO甜熔,全稱(chēng)為Key-Value Observing圆恤,是iOS中的一種設(shè)計(jì)模式,用于檢測(cè)對(duì)象的某些屬性的實(shí)時(shí)變化情況并作出響應(yīng)。鍵值觀(guān)察Key-Value-Observer就是觀(guān)察者模式盆昙。
觀(guān)察者模式的定義:一個(gè)目標(biāo)對(duì)象管理所有依賴(lài)于它的觀(guān)察者對(duì)象羽历,并在它自身的狀態(tài)改變時(shí)主動(dòng)通知觀(guān)察者對(duì)象。這個(gè)主動(dòng)通知通常是通過(guò)調(diào)用各觀(guān)察者對(duì)象所提供的接口方法來(lái)實(shí)現(xiàn)的淡喜。觀(guān)察者模式較完美地將目標(biāo)對(duì)象與觀(guān)察者對(duì)象解耦。
KVO和KVC沒(méi)有什么關(guān)系,要說(shuō)有關(guān)系的話(huà)也就是--KVO同KVC一樣都依賴(lài)于Runtime的動(dòng)態(tài)機(jī)制.
在WPF中有一種雙向綁定機(jī)制澎嚣,如果數(shù)據(jù)模型修改了之后會(huì)立即反映到UI視圖上易桃,類(lèi)似的還有如今比較流行的基于MVVM設(shè)計(jì)模式的前端框架晤郑。其實(shí)在ObjC中原生就支持這種機(jī)制造寝,它叫做Key Value Observing(簡(jiǎn)稱(chēng)KVO)吭练。KVO其實(shí)是一種觀(guān)察者模式线脚,利用它可以很容易實(shí)現(xiàn)視圖組件和數(shù)據(jù)模型的分離浑侥,當(dāng)數(shù)據(jù)模型的屬性值改變之后作為監(jiān)聽(tīng)器的視圖組件就會(huì)被激發(fā)寓落,激發(fā)時(shí)就會(huì)回調(diào)監(jiān)聽(tīng)器自身伶选。在ObjC中要實(shí)現(xiàn)KVO則必須實(shí)現(xiàn)NSKeyValueObServing協(xié)議,不過(guò)幸運(yùn)的是NSObject已經(jīng)實(shí)現(xiàn)了該協(xié)議构资,因此幾乎所有的ObjC對(duì)象都可以使用KVO吐绵。
二己单、怎么實(shí)現(xiàn) KVO
1.注冊(cè)
1 //keyPath就是要觀(guān)察的屬性值 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?2//options給你觀(guān)察鍵值變化的選擇 ? ? ? ? ? ? ? ? ??
3//context方便傳輸你需要的數(shù)據(jù)
4-(void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath5options:(NSKeyValueObservingOptions)options context:(void*)context;
2.實(shí)現(xiàn)
1 //change里存儲(chǔ)了一些變化的數(shù)據(jù)纹份,比如變化前的數(shù)據(jù)廷痘,變化后的數(shù)據(jù);如果注冊(cè)時(shí)context不為空蠢笋,這里context就能接收到昨寞。 ? ?2 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object34change:(NSDictionary *)change context:(void*)context
3 移除
1//移除 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 2- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
三援岩、 KVO底層實(shí)現(xiàn)分析
系統(tǒng)實(shí)現(xiàn)KVO有以下幾個(gè)步驟:
當(dāng)類(lèi)A的對(duì)象第一次被觀(guān)察的時(shí)候享怀,系統(tǒng)會(huì)在運(yùn)行期動(dòng)態(tài)創(chuàng)建類(lèi)A的派生類(lèi)添瓷。我們稱(chēng)為B鳞贷。
在派生類(lèi)B中重寫(xiě)類(lèi)A的setter方法搀愧,B類(lèi)在被重寫(xiě)的setter方法中實(shí)現(xiàn)通知機(jī)制咱筛。
類(lèi)B重寫(xiě)會(huì) class方法杆故,將自己偽裝成類(lèi)A处铛。類(lèi)B還會(huì)重寫(xiě)dealloc方法釋放資源罢缸。
系統(tǒng)將所有指向類(lèi)A對(duì)象的isa指針指向類(lèi)B的對(duì)象。
通俗一點(diǎn)的解釋是:當(dāng)注冊(cè)觀(guān)察者的時(shí)候做的哪些事情:
1.動(dòng)態(tài)的創(chuàng)建一個(gè)叫NSKVONotifying_Person的子類(lèi)
2.更改之前類(lèi)的 isa指針為子類(lèi)
3.傳入一堆參數(shù) 1.監(jiān)聽(tīng)者(將來(lái)調(diào)用observeValueForKeyPath)? 2.keypath(決定了重寫(xiě)哪個(gè)set方法)? 3.枚舉(決定傳哪些給你) 4.攜帶參數(shù)
4.根據(jù)keypath 重寫(xiě)子類(lèi)的set方法
//其實(shí)在子類(lèi)的set方法中是實(shí)現(xiàn)了下面三步
[super setWeight:weight];
//這兩個(gè)方法會(huì)調(diào)用監(jiān)聽(tīng)者的監(jiān)聽(tīng)者方法
[self willChangeValueForKey:@"weight"];
[self didChangeValueForKey:@"weight"];
5.在子類(lèi)的set方法中? 根據(jù)枚舉 保存所有的屬性值? 然后調(diào)用父類(lèi)的set方法 然后調(diào)用監(jiān)聽(tīng)者的observeValueForKeyPath... 把對(duì)應(yīng)的值傳出去通知監(jiān)聽(tīng)者發(fā)生了事情。所以不能依靠isa指針來(lái)確定對(duì)象是否是一個(gè)類(lèi)的成員寝贡。應(yīng)該使用class方法來(lái)確定對(duì)象實(shí)例的類(lèi)圃泡。
四颇蜡、KVO使用陷阱介紹:
首先辆亏,看一下KVO的使用場(chǎng)景,假設(shè)我們的目標(biāo)是在一個(gè)UITableViewController內(nèi)對(duì)tableview的contentOffset進(jìn)行實(shí)時(shí)監(jiān)測(cè)缤弦,很容易地使用KVO來(lái)實(shí)現(xiàn)為[使用場(chǎng)景]。
在初始化方法中加入:
1[_tableView addObserver:self forKeyPath:@"contentOffset"options:NSKeyValueObservingOptionNew context:nil];
在dealloc中移除KVO監(jiān)聽(tīng):
1[_tableView removeObserver:self forKeyPath:@"contentOffset"context:nil];
添加默認(rèn)的響應(yīng)回調(diào)方法:
1- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object3change:(NSDictionary *)change context:(void*)context5{7[self doSomethingWhenContentOffsetChanges];9}
通常的寫(xiě)法已經(jīng)完成,但是當(dāng)你在controller中添加多個(gè)KVO時(shí),所有的回調(diào)都是走同上述函數(shù)累提,那就必須對(duì)觸發(fā)回調(diào)函數(shù)的來(lái)源進(jìn)行判斷恍箭。判斷如下:
1- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
2change:(NSDictionary *)change context:(void*)context
3{
4if(object== _tableView && [keyPath isEqualToString:@"contentOffset"]) {
5[self doSomethingWhenContentOffsetChanges];
6}
7? }
接著還有其他的陷阱,如 我們假設(shè)當(dāng)前類(lèi)(在例子中為UITableViewController)還有父類(lèi)扯夭,并且父類(lèi)也有自己綁定了一些其他KVO. 我們看到交洗,上述回調(diào)函數(shù)體中只有一個(gè)判斷,如果這個(gè)if不成立咆爽,這次KVO事件的觸發(fā)就會(huì)到此中斷了符糊。但事實(shí)上呛凶,若當(dāng)前類(lèi)無(wú)法捕捉到這個(gè)KVO漾稀,那很有可能是在他的superClass崭捍,或者super-superClass...中殷蛇,上述處理截?cái)嗔诉@個(gè)鏈晾咪。合理的處理方式應(yīng)該是這樣的:
1- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
2change:(NSDictionary *)change context:(void*)context
3{
4if(object== _tableView && [keyPath isEqualToString:@"contentOffset"]) {
5[self doSomethingWhenContentOffsetChanges];
6}else{
7[super observeValueForKeyPath:keyPath ofObject:objectchange:change context:context];
8}
9}
還有潛在的問(wèn)題有可能出現(xiàn)在dealloc中對(duì)KVO的注銷(xiāo)上塞赂。KVO的一種缺陷(其實(shí)不能稱(chēng)為缺陷宴猾,應(yīng)該稱(chēng)為特性)是仇哆,當(dāng)對(duì)同一個(gè)keypath進(jìn)行兩次removeObserver時(shí)會(huì)導(dǎo)致程序crash讹剔,這種情況常常出現(xiàn)在父類(lèi)有一個(gè)kvo延欠,父類(lèi)在dealloc中remove了一次由捎,子類(lèi)又remove了一次的情況下狞玛。不要以為這種情況很少出現(xiàn)软驰!當(dāng)你封裝framework開(kāi)源給別人用或者多人協(xié)作開(kāi)發(fā)時(shí)是有可能出現(xiàn)的,而且這種crash很難發(fā)現(xiàn)心肪。不知道你發(fā)現(xiàn)沒(méi)锭亏,目前的代碼中context字段都是nil,那能否利用該字段來(lái)標(biāo)識(shí)出到底kvo是superClass注冊(cè)的蒙畴,還是self注冊(cè)的贰镣?
回答是可以的。我們可以分別在父類(lèi)以及本類(lèi)中定義各自的context字符串膳凝,比如在本類(lèi)中定義context為@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer時(shí)指定移除的自身添加的observer。這樣iOS就能知道移除的是自己的kvo恭陡,而不是父類(lèi)中的kvo蹬音,避免二次remove造成crash。