簡介
什么是KVO揖庄?KVO是Key-Value Observing的簡稱,翻譯成中文就是鍵值觀察欠雌。這是iOS支持的一種機制蹄梢,用來做什么呢?我們在開發(fā)應用時經(jīng)常需要進行通信富俄,比如一個model的某個數(shù)據(jù)變化了禁炒,界面上要進行相應的變化,但是如果我們程序并不知道數(shù)據(jù)什么時候會進行變化霍比,總不能一直循環(huán)判斷有沒有變化吧幕袱,那么就需要在數(shù)據(jù)變化時給controlller發(fā)送一個通知,告知我變化了悠瞬,你可以更新顯示內(nèi)容了们豌,通知的方式有很多種涯捻,比如Notification也是其中一種方式,本文要講解的KVO也是其中一種很輕巧的方式望迎。
他的實現(xiàn)機制為障癌,為可能改變的數(shù)據(jù)增加一個觀察者,在上面的說法中這個觀察者就是controller辩尊,它去觀察這個數(shù)據(jù)有沒有發(fā)生變化涛浙,一旦發(fā)生變化,就會得到一個信號摄欲,從而獲取到變化的數(shù)據(jù)轿亮,進行自己要做的操作。
實例效果
如上圖所示蒿涎,界面上設置兩個label哀托,一個顯示名字,一個顯示分數(shù)劳秋。還有一個按鈕仓手,用來修改分數(shù),現(xiàn)在要做到點擊按鈕分數(shù)變化玻淑。
可能你會覺得很簡單嗽冒,直接在按鈕的響應方法中將分數(shù)的label內(nèi)容修改不就可以了嗎,確實如此补履,但是這里我們不這么做添坊,而是使用KVO來完成。
我們創(chuàng)建一個學生模型箫锤,這個模型有兩個屬性贬蛙,一個為姓名,一個為分數(shù)谚攒。label這是讀取模型的數(shù)據(jù)來進行顯示阳准。
現(xiàn)在我們給這個實例化了的學生模型添加一個觀察者,定義為我要觀察學生模型的分數(shù)變化情況馏臭,這時野蝇,如果這個學生模型的分數(shù)發(fā)生了變化,比如在按鈕響應中只對模型的分數(shù)屬性進行修改括儒,KVO這個機制就會自動給觀察者發(fā)送通知绕沈,說這個屬性變化了,你要做什么操作趕緊做帮寻。
于是我們在觀察者的KVO回調(diào)函數(shù)中進行相應的操作乍狐,如果我們收到了分數(shù)變化的通知,那么就將分數(shù)label的值給修改為當前的分數(shù)规婆。這樣就實現(xiàn)了一套KVO鍵值觀察的流程澜躺,當然最后還缺一步就是移除觀察者蝉稳,不過要在確實需要移除的時候再移除,因為移除后就不再會收到變化的通知了掘鄙。
實現(xiàn)方式
上面例子中進行了一套KVO鍵值觀察的流程耘戚,我們整理一下進行了哪些工作:
- 設計界面樣式
- 建立學生模型
- 對學生的分數(shù)屬性添加觀察
- 修改學生的分數(shù)屬性
- 在觀察到變化的響應方法中進行界面更新操作
- 不再需要觀察的時候移除觀察
現(xiàn)在通過這個例子來一步步講解。
設計樣式
樣式就不說了操漠,兩個label收津,一個按鈕,以及按鈕的響應方法浊伙,都是很常見的撞秋。
建立模型
這個部分,就是新建一個NSObject類嚣鄙,用來作為學生模型吻贿,有兩個屬性:姓名和分數(shù),如下所示:
// StudentModel.h
@interface StudentModel : NSObject
@property (nonatomic, copy) NSString *name;
@property float score;
@end
// ViewController.m
// 在controller中實例化學生模型
self.studentModel = [[StudentModel alloc] init];
[self.studentModel setValue:@"Cloudox" forKey:@"name"];
[self.studentModel setValue:@"89.0" forKey:@"score"];
添加觀察
這一步哑子,才是真正開始使用KVO了舅列。
要使用KVO,至少必須要實現(xiàn)兩個方法:
- addObserver:forkeyPath:options:context:
- observeValueForKeyPath:ofObject:change:context:
第二個方法卧蜓,就是用來獲取數(shù)據(jù)變化的通知并進行相應操作的方法帐要,這個我們后面再講,先講第一個方法弥奸,顧名思義榨惠,這就是用來添加觀察者的方法了。
可能你會注意到盛霎,我們上面實例化學生模型的時候赠橙,使用的是 setVlue:forKey: 的形式來設置屬性值的,為什么要這樣設置呢愤炸?聯(lián)想到KVO的名字简烤,鍵值觀察,就能大概明白了摇幻,學生模型的屬性名就相當于key,屬性值就相當于值挥萌。
緊接著就可以對分數(shù)來添加觀察了:
[self.studentModel addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew context:nil];
這里使用的就是第一個方法绰姻,有四個參數(shù)。
- 第一個參數(shù)是觀察者引瀑,這里被觀察者是學生模型狂芋,觀察者是controller,也就是self
- 第二個參數(shù)是keyPath憨栽,其實也就是要觀察的鍵
- 第三個是一個options帜矾,這里我們寫的是一個枚舉值翼虫,這個地方可以填幾種值,下面在進行詳細的說明
- 第四個我們填了nil屡萤,也有其作用珍剑,下面再細說
總之通過這行代碼,我們就對score這個鍵死陆,也即是分數(shù)添加了觀察招拙。
修改數(shù)據(jù)
在按鈕的響應方法中修改學生模型的分數(shù)數(shù)據(jù),同樣使用 setVlue:forKey: 的方式進行設置措译。
接收通知
這里就用到第二個方法:observeValueForKeyPath:ofObject:change:context:
先看看這個例子中的實現(xiàn):
// KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"score"]) {
self.scoreLabel.text = [NSString stringWithFormat:@"Score:%@", [self.studentModel valueForKey:@"score"]];
}
}
可以看到這個回調(diào)的變化響應方法也有四個參數(shù)别凤,其實就對應上面添加觀察時的四個參數(shù),通過keyPath领虹,我們可以判斷是不是我們想要接收的數(shù)據(jù)變化规哪,判斷它是不是score,其實也就是對不同的被觀察者進行不同的操作塌衰。確實是分數(shù)變化后诉稍,我們就更新界面上的分數(shù)label,用新的分數(shù)來顯示猾蒂。
移除通知
移除通知的方法很簡單均唉,如下:
[self.studentModel removeObserver:self forKeyPath:@"score"];
從觀察者那邊移除對被觀察者特定鍵的觀察。
至此肚菠,一個簡單的KVO流程就走完了舔箭,很簡單對吧。
進階用法
傳遞對象
上面添加觀察者和響應變化的方法中都有一個 context參數(shù)蚊逢,通過這個參數(shù)可以傳遞一些東西层扶,在添加觀察者時設置要傳遞的內(nèi)容,在響應變化時獲得傳遞的內(nèi)容烙荷。
比如我要傳遞一個字符串镜会,在添加觀察者時設置:
[self.studentModel addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew context:@"heyMe"];// 2.通過context傳遞內(nèi)容給觀察者
這里在context的參數(shù)中就直接設置了要傳遞的對象字符串。然后在變化響應時:
// KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"score"]) {
self.scoreLabel.text = [NSString stringWithFormat:@"Score:%@", [self.studentModel valueForKey:@"score"]];
}
NSLog(@"%@", context);// 通過context獲取被觀察者傳遞的內(nèi)容
}
這里就可以輸出context看一下终抽,會得到傳遞過來的字符串內(nèi)容戳表。
options參數(shù)
在添加觀察者時有一個options參數(shù),在回調(diào)獲取變化時有一個change參數(shù)昼伴,這兩個參數(shù)其實是對應的匾旭,都是用來增加傳遞變化的豐富度。
options參數(shù)可以設為:
- NSKeyValueObservingOptionOld:這表示在回調(diào)獲取變化時可以通過change參數(shù)獲取變化之前的值圃郊;
- NSKeyValueObservingOptionNew:這表示在回調(diào)獲取變化時可以通過change參數(shù)獲取變化后的值价涝;
- NSKeyValueObservingOptionInitial:在添加觀察者方法return的時候就發(fā)出一次通知;
- NSKeyValueObservingOptionPrior:會在觀察的值發(fā)生變化前發(fā)出一次通知持舆,變化后還是會發(fā)出一次通知色瘩,所以變化一次一共會得到兩次通知伪窖。
以上就是options參數(shù),可以看到都是對應change參數(shù)的居兆,用來決定change參數(shù)可以得到什么樣的數(shù)據(jù)覆山,在回調(diào)獲取變化時可以輸出change看一下,就可以知道不同的效果了史辙。
change參數(shù)
在使用change的時候可以通過下面的key來操作:
- NSKeyValueChangeKindKey:對應NSKeyValueChange的枚舉值
- NSKeyValueChangeSetting = 1:說明被觀察的數(shù)據(jù)的setter方法被調(diào)用了汹买;
- NSKeyValueChangeInsertion = 2:當觀察的數(shù)據(jù)是集合時,且對它進行insert操作時會返回該值聊倔;
- NSKeyValueChangeRemoval = 3:當觀察的數(shù)據(jù)是集合時晦毙,且對它進行remove操作時會返回該值;
- NSKeyValueChangeReplacement = 4:當觀察的數(shù)據(jù)是集合時耙蔑,且對它進行replace操作時會返回該值见妒。
- NSKeyValueChangeNewKey:對應options參數(shù)中的NSKeyValueObservingOptionNew,會在其中包含觀察的數(shù)據(jù)變化后的新值
- NSKeyValueChangeOldKey:對應options參數(shù)中的NSKeyValueObservingOptionOld甸陌,會在其中包含觀察的數(shù)據(jù)變化之前得舊值
- NSKeyValueChangeIndexesKey:當NSKeyValueChangeKindKey是2须揣、3、4的時候钱豁,也就是說是觀察集合數(shù)據(jù)時耻卡,這個key的值是一個NSIndexSet,包含操作對象的索引集合
- NSKeyValueChangeNotificationIsPriorKey:包含一個布爾值牲尺,如果options的參數(shù)是NSKeyValueObservingOptionPrior卵酪,也就是會通知兩次,在第一次通知谤碳,也就是改變前的通知時溃卡,會包含這個key
關于這些change的值,都可以輸出到控制臺試一下看看是什么效果蜒简,會有更加直觀的感受瘸羡。
手動通知
之前說的都是自動通知,當添加了觀察者后搓茬,只要發(fā)生改變就會自動通知觀察者犹赖,但有時候我們并不是什么改變都希望得到通知,或者有時候是希望變化到什么情況后再通知卷仑,這就需要改變通知的機制冷尉。默認的實現(xiàn)模式為自動通知的模式,要自定義何時進行通知系枪,就要改成手動通知的模式。
要改成手動通知磕谅,首先要在被觀察者的模型中重寫一個方法 automaticallyNotifiesObserversForKey :
// StudentModel.m
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = NO;
if ([key isEqualToString:@"score"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
這里我們在學生模型的實現(xiàn)文件中重寫了這個方法私爷,判斷當觀察的key是score分數(shù)時幔荒,就將自動通知關閉带饱,其余的情況還是根據(jù)父類來進行判斷,這樣寫比較保險。
這樣在我們改變學生模型的分數(shù)時骡楼,就不會自動觸發(fā)通知了,要觸發(fā)通知廉白,需要自己進行設置:
// 按鈕響應
- (void)changeScore {
[self willChangeValueForKey:@"score"];// 改為手動通知
[self.studentModel setValue:@"99.0" forKey:@"score"];
[self didChangeValueForKey:@"score"];// 改為手動通知
}
這時就可以觸發(fā)通知了隧饼,如果一個操作會觸發(fā)多個屬性改變,都要發(fā)通知助币,那么需要嵌套通知:
// 按鈕響應
- (void)changeScore {
[self willChangeValueForKey:@"name"];// 改為手動通知
[self willChangeValueForKey:@"score"];// 改為手動通知
[self.studentModel setValue:@"Cloud" forKey:@"name"];
[self.studentModel setValue:@"99.0" forKey:@"score"];
[self didChangeValueForKey:@"score"];// 改為手動通知
[self didChangeValueForKey:@"name"];// 改為手動通知
}
而在一個一對多的關系中浪听,比如集合,你必須注意不僅僅是這個key改變了眉菱,還有它改變的類型以及索引迹栓,也就是我們change中對應的幾種涉及到集合的東西,如下所示:
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
}
鍵值依賴
其實關于KVO還有一個重要的點是鍵值依賴俭缓,也就是說一個屬性的值依賴于對象中的其他屬性克伊,當那些屬性變化后,這個屬性的值自動被通知到進行修改华坦,不過這個點沒太弄明白愿吹,蘋果給的例子有點不清不楚的,再研究一下吧惜姐。
示例工程:https://github.com/Cloudox/KVODemo