iOS開發(fā)---圖解KVO

什么是KVO?

KVO 全稱 Key Value Observing,是蘋果提供的一套事件通知機制。允許對象監(jiān)聽另一個對象特定屬性的改變,并在改變時接收到事件。由于 KVO 的實現(xiàn)機制,只針對屬性才會發(fā)生作用,一般繼承自 NSObject 的對象都默認支持 KVO。

KVO 可以監(jiān)聽單個屬性的變化,也可以監(jiān)聽集合對象的變化。通過 KVCmutableArrayValueForKey: 等方法獲得代理對象,當代理對象的內(nèi)部對象發(fā)生改變時,會回調(diào) KVO 監(jiān)聽的方法。集合對象包含 NSArrayNSSet。

KVO基本使用

  • 使用KVO大致分為三個步驟:
    1. 通過addObserver:forKeyPath:options:context:方法注冊觀察者,觀察者可以接收keyPath屬性的變化事件
    2. 在觀察者中實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法,當keyPath屬性發(fā)生改變后,KVO會回調(diào)這個方法來通知觀察者
    3. 當觀察者不需要監(jiān)聽時,可以調(diào)用removeObserver:forKeyPath:方法將KVO移除。需要注意的是,調(diào)用removeObserver需要在觀察者消失之前,否則會導致Crash

注冊觀察者

 /*
@observer:就是觀察者,是誰想要觀測對象的值的改變。
@keyPath:就是想要觀察的對象屬性。
@options:options一般選擇NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,這樣當屬性值發(fā)生改變時我們可以同時獲得舊值和新值,如果我們只填NSKeyValueObservingOptionNew則屬性發(fā)生改變時只會獲得新值。
@context:想要攜帶的其他信息,比如一個字符串或者字典什么的。
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

監(jiān)聽回調(diào)

/*
@keyPath:觀察的屬性
@object:觀察的是哪個對象的屬性
@change:這是一個字典類型的值,通過鍵值對顯示新的屬性值和舊的屬性值
@context:上面添加觀察者時攜帶的信息
*/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

調(diào)用方式

自動調(diào)用

  • 調(diào)用KVO屬性對象時,不僅可以通過點語法和set語法進行調(diào)用,還可以使用KVC方法
//通過屬性的點語法間接調(diào)用
objc.name = @"";

// 直接調(diào)用set方法
[objc setName:@"Savings"];
 
// 使用KVC的setValue:forKey:方法
[objc setValue:@"Savings" forKey:@"name"];
 
// 使用KVC的setValue:forKeyPath:方法
[objc setValue:@"Savings" forKeyPath:@"account.name"];

手動調(diào)用

  • KVO 在屬性發(fā)生改變時的調(diào)用是自動的,如果想要手動控制這個調(diào)用時機,或想自己實現(xiàn) KVO 屬性的調(diào)用,則可以通過 KVO 提供的方法進行調(diào)用。

    1. 第一步我們需要認識下面這個方法,如果想要手動調(diào)用或自己實現(xiàn)KVO需要重寫該方法該方法返回YES表示可以調(diào)用,返回NO則表示不可以調(diào)用。
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
        BOOL automatic = NO;
        if ([theKey isEqualToString:@"name"]) {
            automatic = NO;//對該key禁用系統(tǒng)自動通知,若要直接禁用該類的KVO則直接返回NO;
        }
        else {
            automatic = [super automaticallyNotifiesObserversForKey:theKey];
        }
        return automatic;
    }
    
    1. 第二步我們需要重寫setter方法
    - (void)setName:(NSString *)name {
        if (name != _name) {
            [self willChangeValueForKey:@"name"];
            _name = name;
            [self didChangeValueForKey:@"name"];
        }
    }
    

移除觀察者

//需要在不使用的時候,移除監(jiān)聽
- (void)dealloc{
    [self removeObserver:self forKeyPath:@"age"];
}

Crash

觀察者未實現(xiàn)監(jiān)聽方法

  • 若觀察者對象 -observeValueForKeyPath:ofObject:change:context: 未實現(xiàn),將會 Crash

    Crash:Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘<ViewController: 0x7f9943d06710>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled

未及時移除觀察者

Crash: Thread 1: EXC_BAD_ACCESS (code=1, address=0x105e0fee02c0)

//觀察者ObserverPersonChage
@interface ObserverPersonChage : NSObject
  //實現(xiàn)observeValueForKeyPath: ofObject: change: context:
@end

//ViewController
- (void)addObserver
{
    self.observerPersonChange = [[ObserverPersonChage alloc] init];
    [self.person1 addObserver:self.observerPersonChange forKeyPath:@"age" options:option context:@"age chage"];
    [self.person1 addObserver:self.observerPersonChange forKeyPath:@"name" options:option context:@"name change"];
}

//點擊按鈕將觀察者置為nil,即銷毀
- (IBAction)clearObserverPersonChange:(id)sender {
    self.observerPersonChange = nil;
}

//點擊改變person1屬性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 29;
    self.person1.name = @"hengcong";
}
  1. 假如在當前 ViewController 中,注冊了觀察者,點擊屏幕,改變被觀察對象 person1 的屬性值。
  2. 點擊對應(yīng)按鈕,銷毀觀察者,此時 self.observerPersonChange 為 nil。
  3. 再次點擊屏幕,此時 Crash;

多次移除觀察者

Cannot remove an observer for the key path “age” from because it is not registered as an observer.

實際應(yīng)用

KVO主要用來做鍵值觀察操作,想要一個值發(fā)生改變后通知另一個對象,則用KVO實現(xiàn)最為合適。斯坦福大學的iOS教程中有一個很經(jīng)典的案例,通過KVOModelController之間進行通信。

MVC.jpg

KVO實現(xiàn)原理

KVO是通過isa 混寫(isa-swizzling)技術(shù)實現(xiàn)的(是不是一臉懵逼?我第一次見和你一樣,你現(xiàn)在只需要知道這個技術(shù)就行了,下面我會圖文并茂的給你講解到底是怎么回事。)。在運行時根據(jù)原類創(chuàng)建一個中間類,這個中間類是原類的子類,并動態(tài)修改當前對象的isa指向中間類。并且將class方法重寫,返回原類的Class。所以蘋果建議在開發(fā)中不應(yīng)該依賴isa指針,而是通過class實例方法來獲取對象類型。

測試代碼

NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    
NSLog(@"person1添加KVO監(jiān)聽對象之前-類對象 -%@", object_getClass(self.person1));
NSLog(@"person1添加KVO監(jiān)聽之前-方法實現(xiàn) -%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person1添加KVO監(jiān)聽之前-元類對象 -%@", object_getClass(object_getClass(self.person1)));
    
[self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
    
NSLog(@"person1添加KVO監(jiān)聽對象之后-類對象 -%@", object_getClass(self.person1));
NSLog(@"person1添加KVO監(jiān)聽之后-方法實現(xiàn) -%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"person1添加KVO監(jiān)聽之后-元類對象 -%@", object_getClass(object_getClass(self.person1)));

//打印結(jié)果
KVO-test[1214:513029] person1添加KVO監(jiān)聽對象之前-類對象 -Person
KVO-test[1214:513029] person1添加KVO監(jiān)聽之前-方法實現(xiàn) -0x100411470
KVO-test[1214:513029] person1添加KVO監(jiān)聽之前-元類對象 -Person
  
KVO-test[1214:513029] person1添加KVO監(jiān)聽對象之后-類對象 -NSKVONotifying_Person
KVO-test[1214:513029] person1添加KVO監(jiān)聽之后-方法實現(xiàn) -0x10076c844
KVO-test[1214:513029] person1添加KVO監(jiān)聽之后-元類對象 -NSKVONotifying_Person
  
//通過地址查找方法
(lldb) p (IMP)0x10f24b470
(IMP) $0 = 0x000000010f24b470 (KVO-test`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x10f5a6844
(IMP) $1 = 0x000000010f5a6844 (Foundation`_NSSetLongLongValueAndNotify)
  • 通過測試代碼,我們添加KVO前后發(fā)生以下變化
    1. person指向的類對象元類對象,以及 setAge: 均發(fā)生了變化;
    2. 添加KVO后,person 中的 isa 指向了 NSKVONotifying_Person 類對象;
    3. 添加 KVO 之后,setAge: 的實現(xiàn)調(diào)用的是:Foundation 中 _NSSetLongLongValueAndNotify 方法;

發(fā)現(xiàn)中間對象

從上述測試代碼的結(jié)果我們發(fā)現(xiàn),person 中的 isa 從開始指向Person類對象,變成指向了 NSKVONotifying_Person 類對象

  • KVO會在運行時動態(tài)創(chuàng)建一個新類,將對象的isa指向新創(chuàng)建的類,新類是原類的子類,命名規(guī)則是NSKVONotifying_xxx的格式。

    1. 未使用KVO監(jiān)聽對象是,對象和類對象之間的關(guān)系如下
未使用KVO監(jiān)聽對象.png
  1. 使用KVO監(jiān)聽對象后,對象和類對象之間會添加一個中間對象
KVO生成中間對象.png
NSKVONotifying_Person類內(nèi)部實現(xiàn)

我們從上面兩張圖很清楚的看到添加KVO之前和KVO之后的變化,下面我們剖析一下這個中間類NSKVONotifying_Person(這里是*通配符,它代表數(shù)據(jù)類型,例如:int, longlong)

- (void)setAge:(int)age{
    _NSSet*ValueAndNotify();//這個方法調(diào)用順序是什么,它是在調(diào)用何處方法,都在setter方法改變中詳解
}

- (Class)class {
    return [LDPerson class];
}

- (void)dealloc {
    // 收尾工作
}

- (BOOL)_isKVOA {
    return YES;
}

  • isa混寫之后如何調(diào)用方法
    1. 調(diào)用監(jiān)聽的屬性設(shè)置方法,如 setAge:,都會先調(diào)用 NSKVONotify_Person 對應(yīng)的屬性設(shè)置方法;
    2. 調(diào)用非監(jiān)聽屬性設(shè)置方法,如 test,會通過 NSKVONotify_Personsuperclass,找到 Person 類對象,再調(diào)用其 [Person test] 方法
  • 為什么重寫class方法
    • 如果沒有重寫class方法,當該對象調(diào)用class方法時,會在自己的方法緩存列表,方法列表,父類緩存,方法列表一直向上去查找該方法,因為class方法是NSObject中的方法,如果不重寫最終可能會返回NSKVONotifying_Person,就會將該類暴露出來,也給開發(fā)者造成困擾,寫的是Person,添加KVO之后class方法返回怎么是另一個類。
  • _isKVOA有什么作用
    • 這個方法可以當做使用了KVO的一個標記,系統(tǒng)可能也是這么用的。如果我們想判斷當前類是否是KVO動態(tài)生成的類,就可以從方法列表中搜索這個方法。
setter實現(xiàn)不同
  • 在測試代碼中,我們已經(jīng)通過地址查找添加KVO前后調(diào)用的方法

  • //通過地址查找方法
    //添加KVO之前
    (lldb) p (IMP)0x10f24b470
    (IMP) $0 = 0x000000010f24b470 (KVO-test`-[Person setAge:] at Person.h:15)
    //添加KVO之后
    (lldb) p (IMP)0x10f5a6844
    (IMP) $1 = 0x000000010f5a6844 (Foundation`_NSSetLongLongValueAndNotify)
    
    • 0x10f24b470這個地址的setAge:實現(xiàn)是調(diào)用Person類的setAge:方法,并且是在Person.h的第15行。
    • 0x10f5a6844這個地址的setAge:實現(xiàn)是調(diào)用_NSSetIntValueAndNotify這樣一個C函數(shù)。

KVO內(nèi)部調(diào)用流程

  • 由于我們無法去窺探_NSSetIntValueAndNotify的真實結(jié)構(gòu),也無法去重寫NSKVONotifying_Person這個類,所以我們只能利用它的父類Person類來分析其執(zhí)行過程。

    - (void)setAge:(int)age{
        _age = age;
        NSLog(@"setAge:");
    }
    
    - (void)willChangeValueForKey:(NSString *)key{
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
        NSLog(@"didChangeValueForKey - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey - end");
    }
    @end
      
    //打印結(jié)果
    KVO-test[1457:637227] willChangeValueForKey
    KVO-test[1457:637227] setAge:
    KVO-test[1457:637227] didChangeValueForKey - begin
    KVO-test[1457:637227] didChangeValueForKey - end
    KVO-test[1457:637227] willChangeValueForKey
    KVO-test[1457:637227] didChangeValueForKey - begin
    KVO-test[1457:637227] didChangeValueForKey - end
    
    • 通過打印結(jié)果,我們可以清晰看到
      1. 首先調(diào)用willChangeValueForKey:方法。
      2. 然后調(diào)用setAge:方法真正的改變屬性的值。
      3. 開始調(diào)用didChangeValueForKey:這個方法,調(diào)用[super didChangeValueForKey:key]時會通知監(jiān)聽者屬性值已經(jīng)改變,然后監(jiān)聽者執(zhí)行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context這個方法。
  • 下面我用一張圖來展示KVO執(zhí)行流程

    image

KVO擴展

1.KVC 與 KVO 的不同?

  • KVC(鍵值編碼),即 Key-Value Coding,一個非正式的 Protocol,使用字符串(鍵)訪問一個對象實例變量的機制。而不是通過調(diào)用 Setter、Getter 方法等顯式的存取方式去訪問。
  • KVO(鍵值監(jiān)聽),即 Key-Value Observing,它提供一種機制,當指定的對象的屬性被修改后,對象就會接受到通知,前提是執(zhí)行了 setter 方法、或者使用了 KVC 賦值。

2.和 notification(通知)的區(qū)別?

  • KVONSNotificationCenter 都是 iOS 中觀察者模式的一種實現(xiàn)。區(qū)別在于,相對于被觀察者和觀察者之間的關(guān)系,KVO 是一對一的,而不是一對多的。KVO 對被監(jiān)聽對象無侵入性,不需要修改其內(nèi)部代碼即可實現(xiàn)監(jiān)聽。
  • notification 的優(yōu)點是監(jiān)聽不局限于屬性的變化,還可以對多種多樣的狀態(tài)變化進行監(jiān)聽,監(jiān)聽范圍廣,例如鍵盤、前后臺等系統(tǒng)通知的使用也更顯靈活方便。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容