一一罩、KVO簡(jiǎn)介
KVO
是Objective-C
對(duì)觀察者模式(Observer Pattern)
的實(shí)現(xiàn),也是Cocoa Binding
的基礎(chǔ)。當(dāng)被觀察對(duì)象的某個(gè)屬性發(fā)生更改時(shí)撇簿,觀察者對(duì)象會(huì)獲得通知聂渊。
二、KVO的基本使用
- 通過
addObserver:forKeyPath:options:context:
方法注冊(cè)觀察者四瘫,觀察者可以接收keyPath
屬性的變化事件
/*
@observer:觀察者
@keyPath:想要觀察的對(duì)象屬性
@options:options一般選擇NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld汉嗽,這樣當(dāng)屬性值發(fā)生改變時(shí)我們可以同時(shí)獲得舊值和新值,如果我們只填NSKeyValueObservingOptionNew則屬性發(fā)生改變時(shí)只會(huì)獲得新值
@context:想要攜帶的其他信息
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- 在觀察者中實(shí)現(xiàn)
observeValueForKeyPath:ofObject:change:context:
方法找蜜,當(dāng)keyPath
屬性發(fā)生改變后饼暑,KVO
會(huì)回調(diào)這個(gè)方法來通知觀察者
/*
@keyPath:觀察的屬性
@object:觀察的是哪個(gè)對(duì)象的屬性
@change:這是一個(gè)字典類型的值,通過鍵值對(duì)顯示新的屬性值和舊的屬性值
@context:添加觀察者時(shí)攜帶的信息
*/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- 當(dāng)觀察者不需要監(jiān)聽時(shí),可以調(diào)用
removeObserver:forKeyPath:
方法將KVO
移除弓叛。注意調(diào)用removeObserver
需要在觀察者消失之前彰居,否則會(huì)導(dǎo)致Crash
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
三、KVO實(shí)現(xiàn)機(jī)制
KVO
是通過isa
混寫(isa-swizzling)
技術(shù)實(shí)現(xiàn)撰筷。
- 當(dāng)觀察一個(gè)對(duì)象時(shí)陈惰,一個(gè)新的類會(huì)動(dòng)態(tài)被創(chuàng)建(動(dòng)態(tài)添加的類名就是在原來類的類名前加上
NSKVONotifying_類名
); - 這個(gè)類繼承自該對(duì)象原本的類闭专,并重寫被觀察屬性的
setter
方法 - 重寫的
setter
方法會(huì)負(fù)責(zé)在調(diào)用原setter
方法之前和之后,通知所有觀察對(duì)象值的更改 - 最后把這個(gè)對(duì)象的
isa
指針 (isa
指針告訴Runtime
系統(tǒng)這個(gè)對(duì)象的類是什么 ) 指向這個(gè)新創(chuàng)建的子類旧烧,對(duì)象就變成了新創(chuàng)建的子類的實(shí)例影钉。
四、KVO的自動(dòng)觸發(fā)與手動(dòng)觸發(fā)
KVO觀察的開啟和關(guān)閉有兩種方式掘剪,自動(dòng)
和手動(dòng)
自動(dòng)開關(guān)平委,返回NO
,就監(jiān)聽不到夺谁,返回YES
廉赔,表示監(jiān)聽;對(duì)于想要手動(dòng)通知的屬性匾鸥,可以根據(jù)它的keyPath
返回NO
蜡塌,而其對(duì)于其他位置的keyPath
,要返回父類的這個(gè)方法勿负。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要實(shí)現(xiàn)手動(dòng)通知馏艾,你需要在值改變前調(diào)用 willChangeValueForKey:
方法,在值改變后調(diào)用 didChangeValueForKey:
方法奴愉。你可以在發(fā)送通知前檢查值是否改變琅摩,如果沒有改變就不發(fā)送通知。
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != _openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}
使用手動(dòng)開關(guān)的好處就是你監(jiān)聽就監(jiān)聽锭硼,不想監(jiān)聽關(guān)閉即可房资,比自動(dòng)觸發(fā)更方便靈活
如果一個(gè)操作會(huì)導(dǎo)致多個(gè)屬性改變,你需要嵌套通知檀头,像下面這樣:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
在一個(gè)一對(duì)多的關(guān)系中轰异,你必須注意不僅僅是這個(gè)key
改變了,還有它改變的類型以及索引暑始。
- (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一對(duì)多,鍵值依賴
通過注冊(cè)一個(gè)KVO觀察者蒋荚,可以監(jiān)聽多個(gè)屬性的變化
比如目前有一個(gè)需求戳稽,需要根據(jù)總的下載量totalData
和當(dāng)前下載量currentData
來計(jì)算當(dāng)前的下載進(jìn)度currentProcess
,實(shí)現(xiàn)有兩種方式
- 分別觀察
totalData
和currentData
兩個(gè)屬性,當(dāng)其中一個(gè)發(fā)生變化計(jì)算currentProcess
- 實(shí)現(xiàn)
keyPathsForValuesAffectingValueForKey
方法惊奇,將兩個(gè)觀察合為一個(gè)觀察互躬,即觀察當(dāng)前下載進(jìn)度currentProcess
//1、合二為一的觀察方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"currentProcess"]) {
NSArray *affectingKeys = @[@"totalData", @"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
//2颂郎、注冊(cè)KVO觀察
[self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];
//3吼渡、觸發(fā)屬性值變化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.currentData += 10;
self.person.totalData += 1;
}
//4、移除觀察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"currentProcess"];
}
你也可以通過實(shí)現(xiàn) keyPathsForValuesAffecting<Key>
方法來達(dá)到前面同樣的效果乓序,這里的<Key>
就是屬性名寺酪,不過第一個(gè)字母要大寫,用前面的例子來說就是這樣:
+ (NSSet *)keyPathsForValuesAffectingCurrentProcess {
return [NSSet setWithObjects:@"totalData", @"currentData", nil];
}
六替劈、KVO觀察 可變數(shù)組
KVO是基于KVC基礎(chǔ)之上的寄雀,所以可變數(shù)組如果直接添加數(shù)據(jù),是不會(huì)調(diào)用setter
方法的陨献,即直接通過[self.person.dateArray addObject:@"1"];
向數(shù)組添加元素盒犹,是不會(huì)觸發(fā)KVO通知回調(diào)的
在KVC官方文檔中,針對(duì)可變數(shù)組的集合類型眨业,有如下說明急膀,即訪問集合對(duì)象需要需要通過
mutableArrayValueForKey
方法,這樣才能將元素添加到可變數(shù)組中;
// KVC 用此方法添加則可以觸發(fā)KVO
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
七龄捡、其他知識(shí)點(diǎn)
- 通過
KVC
修改屬性會(huì)觸發(fā)KVO
卓嫂,KVC
內(nèi)部做了監(jiān)聽操作 - 直接修改成員變量不會(huì)觸發(fā)
KVO
,沒走set方法 - 哪些情況下使用
kvo
會(huì)崩潰聘殖,怎么防護(hù)崩潰命黔?
1.
removeObserver
一個(gè)未注冊(cè)的keyPath
,導(dǎo)致錯(cuò)誤:Cannot remove an observer A for the key path "str"就斤,because it is not registered as an observer
.
解決辦法:根據(jù)實(shí)際情況悍募,增加一個(gè)添加keyPath的標(biāo)記,在dealloc中根據(jù)這個(gè)標(biāo)記洋机,刪除觀察者坠宴。
2.添加的觀察者已經(jīng)銷毀,但是并未移除這個(gè)觀察者绷旗,當(dāng)下次這個(gè)觀察的keyPath
發(fā)生變化時(shí)喜鼓,kvo
中的觀察者的引用變成了野指針,導(dǎo)致crash
衔肢。
解決辦法:在觀察者即將銷毀的時(shí)候庄岖,先移除這個(gè)觀察者。
其實(shí)還可以將觀察者observer
委托給另一個(gè)類去完成角骤,這個(gè)類弱引用被觀察者隅忿,當(dāng)這個(gè)類銷毀的時(shí)候心剥,移除觀察者對(duì)象
- kvo的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
1.能夠提供一種簡(jiǎn)單的方法實(shí)現(xiàn)兩個(gè)對(duì)象間的同步
2.能夠?qū)Ψ俏覀儎?chuàng)建的對(duì)象,即內(nèi)部對(duì)象的狀態(tài)改變做出響應(yīng)背桐,而且不需要改變內(nèi)部對(duì)象的實(shí)現(xiàn)
3.能夠提供觀察的屬性的最新值以及先前值
4.用key paths來觀察屬性优烧,因此也可以觀察嵌套對(duì)象
5.完成了對(duì)觀察對(duì)象的抽象,因?yàn)椴恍枰~外的代碼來允許觀察值能夠被觀察
缺點(diǎn):
1.我們觀察的屬性必須使用string來定義链峭,因此在編譯期不會(huì)出現(xiàn)警告以及檢查
2.對(duì)屬性重構(gòu)將導(dǎo)致我們的觀察代碼不再可用
3.只能通過重寫 -observeValueForKeyPath:ofObject:change:context:
方法來獲得通知畦娄。
4.不能通過指定selector
的方式獲取通知。
5.不能通過block
的方式獲取通知弊仪。
- 添加觀察者和移除觀察者要相對(duì)應(yīng)熙卡;
- 不要將已經(jīng)釋放的觀察者對(duì)象,再進(jìn)行移除励饵;
- 可以多次對(duì)同一個(gè)屬性添加相同的觀察者驳癌,當(dāng)屬性更改的時(shí)候,會(huì)多次調(diào)用接收方法曲横,不過移除觀察者也要執(zhí)行多次喂柒;
- 在iOS10及其以下不瓶,不移除觀察者會(huì)出現(xiàn)閃退的情況禾嫉,在iOS11及其以上,不會(huì)出現(xiàn)閃退的情況蚊丐;