KVO的使用及底層實現

一、概念

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

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

 

二、使用atom

一、spa

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

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

 

三、指針

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

 

注意點

KVOaddObserverremoveObserver須要是成對的,若是重複remove則會致使NSRangeException類型的Crash,若是忘記remove則會在觀察者釋放後再次接收到KVO回調時Crashcode

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

 

調用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的blog

//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可否監聽到變化  要看這個被監聽對象存儲的是什麼?其實是否發生了改變?

 

三、原理

咱們在經過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動態生成的類,就能夠從方法列表中搜索這個方法。 

 

 

 四、如何手動觸發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{}這個回調方法的

 

 參考資料 
相關文章
相關標籤/搜索