1. 程式人生 > >iOS KVO 鍵值觀察

iOS KVO 鍵值觀察

觀察 model 物件的變化

在 Cocoa 的模型-檢視-控制器 (Model-view-controller)架構裡,控制器負責讓檢視和模型同步。這一共有兩步:當 model 物件改變的時候,檢視應該隨之改變以反映模型的變化;當用戶和控制器互動的時候,模型也應該做出相應的改變。

KVO 能幫助我們讓檢視和模型保持同步。控制器可以觀察檢視依賴的屬性變化。

讓我們看一個例子:我們的模型類 LabColor 代表一種 Lab色彩空間裡的顏色。和 RGB 不同,這種色彩空間有三個元素 L, a, b。我們要做一個用來改變這些值的滑塊和一個顯示顏色的方塊區域。

我們的模型類有以下三個用來代表顏色的屬性:

@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;

依賴的屬性
我們需要從這個類建立一個 UIColor 物件來顯示出顏色。我們新增三個額外的屬性,分別對應 R, G, B:

@property (nonatomic, readonly) double redComponent;
@property (nonatomic, readonly) double greenComponent;
@property
(nonatomic, readonly) double blueComponent; @property (nonatomic, strong, readonly) UIColor *color;

有了這些以後,我們就可以建立這個類的介面了:

@interface LabColor : NSObject

@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;

@property (nonatomic
, readonly) double redComponent; @property (nonatomic, readonly) double greenComponent; @property (nonatomic, readonly) double blueComponent; @property (nonatomic, strong, readonly) UIColor *color; @end

維基百科提供了轉換 RGB 到 Lab 色彩空間的演算法。寫成方法之後如下所示:

- (double)greenComponent;
{
    return D65TristimulusValues[1] * inverseF(1./116. * (self.lComponent + 16) + 1./500. * self.aComponent);
}

[...]

- (UIColor *)color
{
    return [UIColor colorWithRed:self.redComponent * 0.01 green:self.greenComponent * 0.01 blue:self.blueComponent * 0.01 alpha:1.];
}

這些程式碼沒什麼令人激動的地方。有趣的是 greenComponent 屬性依賴於 lComponent 和 aComponent。不論何時設定 lComponent 的值,我們需要讓 RGB 三個 component 中與其相關的成員以及 color 屬性都要得到通知以保持一致。這一點這在 KVO 中很重要。

Foundation 框架提供的表示屬性依賴的機制如下:

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key;

更詳細的如下:

+ (NSSet *)keyPathsForValuesAffecting<鍵名>;

在我們的例子中如下:

+ (NSSet *)keyPathsForValuesAffectingRedComponent
{
    return [NSSet setWithObject:@"lComponent"];
}

+ (NSSet *)keyPathsForValuesAffectingGreenComponent
{
    return [NSSet setWithObjects:@"lComponent", @"aComponent", nil];
}

+ (NSSet *)keyPathsForValuesAffectingBlueComponent
{
    return [NSSet setWithObjects:@"lComponent", @"bComponent", nil];
}

+ (NSSet *)keyPathsForValuesAffectingColor
{
    return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];
}

現在我們完整的表達了屬性之間的依賴關係。請注意,我們可以把這些屬性連結起來。打個比方,如果我們寫一個子類去 override redComponent 方法,這些依賴關係仍然能正常工作。

看完了上面objc的內容,讓我來再深入分析一下

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key;

Discussion

When an observer for the key is registered with an instance of the receiving class, key-value observing itself automatically observes all of the key paths for the same instance, and sends change notifications for the key to the observer when the value for any of those key paths changes.

The default implementation of this method searches the receiving class for a method whose name matches the pattern +keyPathsForValuesAffecting, and returns the result of invoking that method if it is found. Any such method must return an NSSet. If no such method is found, an NSSet that is computed from information provided by previous invocations of the now-deprecated setKeys:triggerChangeNotificationsForDependentKey: method is returned, for backward binary compatibility.

You can override this method when the getter method of one of your properties computes a value to return using the values of other properties, including those that are located by key paths. Your override should typically call super and return a set that includes any members in the set that result from doing that (so as not to interfere with overrides of this method in superclasses).

大意就是,當一個觀察者被註冊的時候,鍵值觀察會觀察該類所有的例項的指定鍵值對,當鍵值對改變的時候傳送通知。該方法的預設實現模式為 + (NSSet )keyPathsForValuesAffectingValueForKey:(NSString )key;
你可以重寫這些getter方法,返回自定義的影響屬性。

+ (NSSet *)keyPathsForValuesAffectingColor
{
    return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];
}

觀察變化

我們把檢視控制器註冊為觀察者來接收 KVO 的通知,這可以用以下 NSObject 的方法來實現:

- (void)addObserver:(NSObject *)anObserver
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context;

這會讓以下方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;

在當 keyPath 的值改變的時候在觀察者 anObserver 上面被呼叫。這個 API 看起來有一點嚇人。更糟糕的是,我們還得記得呼叫以下的方法

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

方法解析

- (void)addObserver:(NSObject *)anObserver
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context;

引數
anObserver
要註冊KVO通知的物件。 觀察者必須實現鍵值觀察方法observeValueForKeyPath:ofObject:change:context :

keyPath
相對於陣列的關鍵路徑,要觀察的屬性。 此值不能為零。

options
NSKeyValueObservingOptions值的組合,指定包含在觀察通知中的內容。

context
在ObserValueForKeyPath:ofObject:change:context:中傳遞給觀察者的任意資料。

討論
NSArray物件不可觀察,因此當在NSArray物件上呼叫時,此方法引發異常。 不要觀察陣列,而要觀察陣列是相關物件集合的一對多關係。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;

Parameters

keyPath
The key path, relative to object, to the value that has changed.

object
The source object of the key path keyPath.

change
A dictionary that describes the changes that have been made to the value of the property at the key path keyPath relative to object. Entries are described in Change Dictionary Keys.

context
The value that was provided when the observer was registered to receive key-value observation notifications.

引數

keyPath
相對於物件的鍵路徑,已更改的值。

object
鍵路徑keyPath的源物件。

change
描述對關鍵路徑keyPath相對於物件的屬性的值所做的更改的字典。 條目在更改字典鍵中描述。

context
註冊觀察者以接收鍵值觀察通知時提供的值。

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

NSArray物件不可觀察,因此當在NSArray物件上呼叫時,此方法引發異常。 不要觀察陣列,而要觀察陣列是相關物件集合的一對多關係。

通知

當我們觀察的key 的 value 放生改變shi,會呼叫以下方法:

- (void)willChangeValueForKey:(NSString *)key;

和:

- (void)didChangeValueForKey:(NSString *)key;

進階KVO

options

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    //如果我們需要改變前後的值,我們可以在 KVO 選項中加入 NSKeyValueObservingOptionNew 和/或 NSKeyValueObservingOptionOld。
    //更簡單的辦法是用 NSKeyValueObservingOptionPrior 選項,隨後我們就可以用以下方式提取出改變前後的值:
    //id oldValue = change[NSKeyValueChangeOldKey];
    //id newValue = change[NSKeyValueChangeNewKey];
    NSKeyValueObservingOptionNew = 0x01,
    NSKeyValueObservingOptionOld = 0x02,
    NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,//初始化(addObserver:)時,KVO就被觸發
    NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08//這能使我們在鍵值改變之前被通知,這和-willChangeValueForKey:被觸發的時間相對應
};

通常來說 KVO 會在 -willChangeValueForKey: 和 -didChangeValueForKey: 被呼叫的時候儲存相應鍵的值。

KVO 的實現

當你觀察一個物件時,一個新的類會動態被建立。這個類繼承自該物件的原本的類,並重寫了被觀察屬性的 setter 方法。自然,重寫的 setter 方法會負責在呼叫原 setter 方法之前和之後,通知所有觀察物件值的更改。最後把這個物件的 isa 指標 ( isa 指標告訴 Runtime 系統這個物件的類是什麼 ) 指向這個新建立的子類,物件就神奇的變成了新建立的子類的例項。
原來,這個中間類,繼承自原本的那個類。不僅如此,Apple 還重寫了 -class 方法,企圖欺騙我們這個類沒有變,就是原本那個類。更具體的資訊,去跑一下 Mike Ash 的那篇文章裡的程式碼就能明白,這裡就不再重複。