KVO(Key-Value Observing)

一一罩、KVO簡(jiǎn)介

KVOObjective-C 對(duì)觀察者模式(Observer Pattern)的實(shí)現(xiàn),也是 Cocoa Binding 的基礎(chǔ)。當(dāng)被觀察對(duì)象的某個(gè)屬性發(fā)生更改時(shí)撇簿,觀察者對(duì)象會(huì)獲得通知聂渊。

二、KVO的基本使用

  1. 通過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;

  1. 在觀察者中實(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;
  1. 當(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)撰筷。

  1. 當(dāng)觀察一個(gè)對(duì)象時(shí)陈惰,一個(gè)新的類會(huì)動(dòng)態(tài)被創(chuàng)建(動(dòng)態(tài)添加的類名就是在原來類的類名前加上NSKVONotifying_類名);
  2. 這個(gè)類繼承自該對(duì)象原本的類闭专,并重寫被觀察屬性的 setter 方法
  3. 重寫的 setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前和之后,通知所有觀察對(duì)象值的更改
  4. 最后把這個(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)有兩種方式

  • 分別觀察totalDatacurrentData 兩個(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)閃退的情況蚊丐;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末熙参,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子麦备,更是在濱河造成了極大的恐慌孽椰,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凛篙,死亡現(xiàn)場(chǎng)離奇詭異黍匾,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)呛梆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門锐涯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人填物,你說我怎么就攤上這事纹腌。” “怎么了滞磺?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵升薯,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我击困,道長(zhǎng)涎劈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮责语,結(jié)果婚禮上炮障,老公的妹妹穿的比我還像新娘。我一直安慰自己坤候,他們只是感情好胁赢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著白筹,像睡著了一般智末。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上徒河,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天系馆,我揣著相機(jī)與錄音,去河邊找鬼顽照。 笑死由蘑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的代兵。 我是一名探鬼主播尼酿,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼植影!你這毒婦竟也來了裳擎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤思币,失蹤者是張志新(化名)和其女友劉穎鹿响,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谷饿,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惶我,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了博投。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绸贡。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贬堵,靈堂內(nèi)的尸體忽然破棺而出恃轩,到底是詐尸還是另有隱情,我是刑警寧澤黎做,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布叉跛,位于F島的核電站,受9級(jí)特大地震影響蒸殿,放射性物質(zhì)發(fā)生泄漏筷厘。R本人自食惡果不足惜鸣峭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酥艳。 院中可真熱鬧摊溶,春花似錦、人聲如沸充石。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)骤铃。三九已至拉岁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惰爬,已是汗流浹背喊暖。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撕瞧,地道東北人陵叽。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像丛版,于是被迫代替她去往敵國(guó)和親巩掺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

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