淺析 KVO 內部實現

KVO 全稱是Key Value Observing,翻譯成鍵值觀察。提供了一種當其它對象屬性被修改的時候能通知當前對象的機制。

KVO 的基本使用:

(1)註冊指定Key路徑的監聽器:

 /** 參數
         *  addObserver: 監聽對象
         *  forKeyPath: 監聽屬性Key
         *  options: 監聽可選項
         *      NSKeyValueObservingOptionNew: 監聽改變後的新值
         *      NSKeyValueObservingOptionOld: 監聽改變後的舊值
         *  context: 傳入的上下文
         */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

(2)刪除指定Key路徑的監聽器:

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

(3)回調監聽:

- (void)observeValueForKeyPath:(NSString *)keyPath //監聽的屬性值
                      ofObject:(id)object          //監聽的對象
                        change:(NSDictionary<NSString *,id> *)change //值的改變(由options參數決定傳入新值或者舊值)
                       context:(void *)context //傳入的上下文內容

值得注意的是:不要忘記解除註冊,否則會導致資源泄露。

設置屬性

將觀察者與被觀察者註冊好之後,就可以對觀察者對象的屬性進行操作,這些變更操作就會被通知給觀察者對象。注意,只有遵循 KVO 方式來設置屬性,觀察者對象纔會獲取通知,也就是說遵循使用屬性的 setter 方法,或通過 key-path 來設置:

target.age = 30;
[target setAge:30]; 
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];

下面看一個小 Demo:
我們設想一個場景,當自己住酒店的時候,當酒店給我換房間的時候,我們要得到提醒,才能找對自己的房間。我們依次爲例:

//ViewController
#import "ViewController.h"
#import "Person.h"
#import "Room.h"

@interface ViewController ()
@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) Room *room;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[Person alloc] init];
    self.room = [[Room alloc] init];
    //設置房間的號碼
    self.room.no = 10;

    //Person 監聽 Room 編號的變化
    [self.room addObserver:self.person forKeyPath:@"no" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //房間號變爲20
    self.room.no = 20;
}

@end

//Person.m 文件
#import "Person.h"

@implementation Person

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"no"]) {
        NSLog(@"Person 檢測到 Room 的屬性: %@ 值改變: %@", keyPath, change);
    }
}

//移除觀察者對象,防止內存泄漏
- (void)dealloc{
    [self.room removeObserver:self forKeyPath:@"no"];
}
@end

運行,我們得到:
結果

KVO 的內部實現

下面我們分析下,KVO 的內部實現:
1> KVO 是基於 runtime 的 isa-swizzing 機制實現的;
2> 當類 A 的對象第一次被觀察的時候,系統會在運行期動態創建類A 的派生類。系統命名爲NSKVONotifying_A。
3> 在派生類 NSKVONotifying_A 中重寫類 A 的setter方法,NSKVONotifying_A類在被重寫的setter方法中實現通知機制。
4> 其中鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey: 在一個被觀察屬性發生改變之前, willChangeValueForKey: 一定會被調用,這就 會記錄舊的值。而當改變發生後,observeValueForKey:ofObject:change:context: 會被調用,繼而 didChangeValueForKey: 也會被調用。如果可以手動實現這些調用,就可以實現“手動觸發”了(後面介紹)。
5> 類 NSKVONotifying_A會重寫 class方法,將自己僞裝成類A。類 NSKVONotifying_A 還會重寫 deallo 方法來釋放資源。
系統將所有指向類 A 對象的isa指針指向類 NSKVONotifying_A 的對象。

爲了證明上述過程:我們第一步註釋掉ViewController 添加觀察者的代碼,在運行的時候,查看類 Room 的 isa 指針的值:
這裏寫圖片描述

當將添加觀察者處的代碼打開,我們觀察到,在運行的時候,Room 的 isa指針指向了NSKVONotifying_Room 類(派生類)
這裏寫圖片描述

KVO 手動實現

在 Room.m 文件中實現:

/**
首先,需要手動實現屬性的 setter 方法,並在設置操作的前後分別調用 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用於通知系統該 key 的屬性值即將和已經變更了;

其次,要實現類方法 automaticallyNotifiesObserversForKey,並在其中設置對該 key 不自動發送通知(返回 NO 即可)。這裏要注意,對其它非手動實現的 key,要轉交給 super 來處理。
*/
#import "Room.h"

@implementation Room

- (void)setNo:(int)no{
    [self willChangeValueForKey:@"no"];
    _no = no;
    [self didChangeValueForKey:@"no"];
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"no"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
@end

當我們再次運行觀察,發現 Room 的 isa 指針指向Room類:
這裏寫圖片描述

參考:

http://www.cppblog.com/kesalin/archive/2012/11/17/kvo.html

申明

以上觀點,屬於個人的理解,如果錯誤之處,歡迎拍磚。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章