KVO的使用及底層實現


1、概念

KVO(Key-Value-Observer)也就是觀察者模式,是蘋果提供的一套事件通知機制。允許對象監聽另一個對象特定屬性的改變,並在改變時接收到事件,一般繼承自NSObject的對象都默認支持KVO

KVO和NSNotificationCenter都是iOS中觀察者模式的一種實現。區別在於:
1、相對於被觀察者和觀察者之間的關系,KVO是一對一的,而不一對多的。也就是kvo監聽到被觀察屬性值改變時只會通知到觀察者,是一對一的關系。而通知模式則是在被觀察值改變的時候發送全局通知,任何對象都可以接聽到這個通知,是一個一對多的關系;
2、KVO對被監聽對象無侵入性,不需要修改其內部代碼即可實現監聽。而通知需要在被監聽對象改變的時候添加發送通知代碼。

 

2、使用

1、

//1.注冊觀察者
    /*
        - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
     
     observer:觀察者  也就是被觀察對象發生改變時通知的接收者
     
     keyPath:被觀察的屬性名   比如我們這里是age屬性
     
     options:參數  這里一般選擇NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld  也就是在回調方法里會受到被觀察屬性的舊值和新值,默認為只接收新值。如果想在注冊觀察者后,立即接收一次回調,則可以加入NSKeyValueObservingOptionInitial枚舉。
     
     context:這個參數可以傳入任意類型的對象,這個值會傳遞到接收消息回調的代碼中,是KVO中的一種傳值方式。

     */
    [self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

 

2、

//2.實現通知回調方法 當被觀察對象的屬性值發生變化時  就會回調這個方法  change字典中存放KVO屬性相關的值,根據options時傳入的枚舉來返回。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@---%@----%@---%@",keyPath,object,change,context);
}

 

3、

  //3.移除監聽
    [self.per1 removeObserver:self forKeyPath:@"age"];

 

注意點

KVOaddObserverremoveObserver需要是成對的,如果重復remove則會導致NSRangeException類型的Crash,如果忘記remove則會在觀察者釋放后再次接收到KVO回調時Crash

蘋果官方推薦的方式是,在init的時候進行addObserver,在deallocremoveObserver,這樣可以保證addremove是成對出現的,是一種比較理想的使用方式。

 

調用KVO屬性對象時,不僅可以通過點語法和set語法進行調用,KVO兼容很多種調用方式:(關於KVC的實現原理接下來會講到)

 // 1.通過屬性的點語法間接調用
    self.per1.age = 123;
//2. 直接調用set方法   [self.per1 setAge:123];
// 3.使用KVC的setValue:forKeyPath:方法 [self.per1 setValue:@123 forKeyPath:@"age"]; //4. 使用KVC的setValue:forKey:方法 [self.per1 setValue:@123 forKey:@"age"]; // 5.通過mutableArrayValueForKey:方法獲取到代理對象,並使用代理對象進行操作

如果直接修改對象的成員變量是不會觸發KVO的

//PersonClass.h文件

#import <Foundation/Foundation.h>
@interface PersonClass : NSObject{
  @public;
NSInteger _age;//成員變量 } //屬性 @property (nonatomic, assign) NSInteger age; @end

直接修改成員變量,我們發現沒有觸發KVO

 self.person1 -> _age = 234;

 

 

 

上面全是監聽一些基礎的數據類型  當被觀察屬性是一個復雜對象時,比如現在person對象有一個屬性animal,那么kvo會如何監聽呢?

#import <Foundation/Foundation.h>
@class AnimalClass;
@interface PersonClass : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) AnimalClass *animal;

@end

AnimalClass類中有一個name屬性

@interface AnimalClass : NSObject
@property (nonatomic, copy) NSString *name;
@end

當我們對animal這個屬性進行監聽時,發現當對animal的屬性值(name)修改時  kvo並不會監聽到,  而當給person對象重新賦值一個新的animalClass對象時會被監聽到

    //會監聽到改變  因為person1的animal屬性是個指針 存儲的是animal類型的一個地址值  當重新賦值一個alloc出來的新animalClass對象時  animal的地址值發生了改變  會調用person1的setAnimal方法
    AnimalClass *ani2 = [[AnimalClass alloc]init];
    ani2.name = @"cat";
    self.person1.animal = ani2;
    
    //不會被kvo監聽到  因為修改animal的name屬性 根本沒有調用person1的setAnimal方法  只是調用了animal的setName方法
    self.person1.animal.name = @"cat";

 

而當我們對person1.animal對象的name屬性進行監聽時  是可以監聽到 self.person1.animal.name = @"cat";這種值改動的

    [self.person1.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

所以kvo能否監聽到變化  要看這個被監聽對象存儲的是什么?實際上是否發生了改變?

 

3、原理

我們在通過runtime函數object_getclass分別打印person1在添加kvo前后的類對象分別是是PersonClass和NSKVONotifying_PersonClass;

也就是在person1對象注冊了kvo以后,其類對象發生了改變 

我們在改變age值的時候  實際上是調用了setAge方法  而實例對象調用方法是根絕isa指針找到類對象的對象方法列表找到對應的方法進行調用,所以kvo的本質實際上是重寫了被觀察屬性值的set方法

NSKVONotifying_PersonClass類對象set方法的具體實現:(_NSSet*ValueAndNotify的內部實現)

didChangeValueForKey:內部會調用observer的observeValueForKeyPath:ofObject:change:context:方法

 

實現原理

KVO是通過isa-swizzling技術實現的(這句話是整個KVO實現的重點)。在運行時利用RuntimeAPI動態生成一個根據原類創建的中間類(命名規則是NSKVONotifying_xxx的格式),這個中間類是原類的子類,並動態修改當前對象的isa指向中間類。

首先重寫set方法。在set方法里分別調用willChangeValueForKey->set的賦值操作->didChangeValueForKey  其中didChangeValueForKey在內部視線中會調用觀察者的回調方法 返回被觀察對象的相關參數

 

並且將class方法重寫,返回原類的Class(PersonClass類)。這是因為蘋果不想暴露kvo的內部實現,建議在開發中不應該依賴isa指針,而是通過class實例方法來獲取對象類型。

 

_isKVOA方法,這個方法可以當做使用了KVO的一個標記,系統可能也是這么用的。如果我們想判斷當前類是否是KVO動態生成的類,就可以從方法列表中搜索這個方法。 

 

 

 4、如何手動觸發KVO

KVO在屬性發生改變時的調用是自動的,如果在被觀察屬性值沒有改變的情況下手動調用kvo 那么需要時候調用willChangeValueForKey和didChangeValueForKey兩個方法(兩個方法必須都進行調用  系統在執行didChangeValueForKey方法前會檢測willChangeValueForKey是否被調用了)

    [self.person1 willChangeValueForKey:@"age"];
    
    [self.person1 didChangeValueForKey:@"age"];

手動觸發的前提是這個對象已經添加了kvo  如果沒有添加的話kvo是無法知道觀察者是誰的 也就是不會回調觀察者的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{}這個回調方法的

 

 參考資料 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM