[轉]KVO & KVC

本文轉自:Objective-C中的KVC和KVO.

  1. KVC
  2. KVO
    2.1. Registering for Key-Value Observing
    2.1.1. 注冊成為觀察者
    2.1.2. 接收變更通知
    2.1.3. 移除觀察者
    2.2. KVO Compliance(KVO兼容)
    2.2.1. Automatic Change Notification(自動通知)
    2.2.2. Manual Change Notification(手動通知)
    2.3. Registering Dependent Keys(注冊依賴鍵)
    2.3.1. To-one Relationships
    2.3.2. To-many Relationships
    2.4. 調試KVO

KVC

鍵/值編碼中的基本調用包括-valueForKey:-setValue:forKey:。以字符串的形式向對象發送消息,這個字符串是我們關注的屬性的關鍵。

valueForKey:首先查找以鍵-key或-isKey命名的getter方法。如果不存在getter方法(假如我們沒有通過@synthesize提供存取方法),它將在對象內部查找名為_key或key的實例變量。

對于KVC,Cocoa自動放入和取出標量值(int,float和struct)放入NSNumber或NSValue中;當使用-setValue:ForKey:時,它自動將標量值從這些對象中取出。僅KVC具有這種自動包裝功能,常規方法調用和屬性語法不具備該功能。

-setValue:ForKey:的工作方式和-valueForKey:相同。它首先查找名稱的setter方法,如果不存在setter方法,它將在類中查找名為_key或key的實例變量。

使用KVC訪問屬性的代價比直接使用存取方法要大,所以只在需要的時候才用。

最簡單的 KVC 能讓我們通過以下的形式訪問屬性:

@property (nonatomic, copy) NSString *name;

取值:

NSString *n = [object valueForKey:@"name"];

設定:

[object setValue:@"Daniel" forKey:@"name"];

值得注意的是這個不僅可以訪問作為對象屬性,而且也能訪問一些標量(例如 int 和 CGFloat)和 struct(例如 CGRect)。Foundation 框架會為我們自動封裝它們。舉例來說,如果有以下屬性:

@property (nonatomic) CGFloat height;

我們可以這樣設置它:

[object setValue:@(20) forKey:@"height"];

有關KVC的更多用法,參看下面的文章:

KVO

KVO是Cocoa提供的一種稱為“鍵-值”觀察的機制,對象可以通過它得到其他對象特性屬性的變更通知。這種機制在MVC模式的場景中很重要,因為它讓視圖對象可以經由控制器層觀察模型對象的變更。
這一機制基于NSKeyValueObserving非正式協議,Cocoa通過這個協議為所有遵守協議的對象提供了一種自動化的屬性觀察能力。要實現自動觀察,參與KVO的對象需要符合KVC的要求和存取方法,也可以手動實現觀察者通知,也可以兩者都保留。

KVO是Cocoa框架使用觀察者模式的一種途徑。

設置一個屬性的觀察者需要三步,理解這些步驟可以更清楚的知道KVO的工作框圖.

首先看看你當前的場景如果使用KVO是否更妥當,比如,當一個實例的某個具體屬性有任何變更的時候,另一個實例需要被通知。

比如,BankObject中的accountBalance屬性有任何變更時,某個PersonObject對象都要覺察到。
這個PersonObject對象必須注冊成為BankObject的accountBalance屬性的觀察者,可以通過發送addObserver: forKeyPath: options: context:消息來實現。

注意:addObserver: forKeyPath: options: context:方法在你指定的兩個實例間建立聯系,而不是在兩個類之間.

為了回應變更通知,觀察者必須實現observeValueForKeyPath: ofObject: change: context:方法。這個方法的實現決定了觀察者如何回應變更通知。你可以在這個方法里自定義如何回應被觀察屬性的變更。

當一個被觀察屬性的值以符合KVO方式變更或者當它依賴的鍵變更時,observeValueForKeyPath: ofObject: change: context:方法會被自動執行。

注冊成為觀察者(Registering for Key-Value Observing)

你可以通過發送addObserver: forKeyPath: options: context:消息來注冊觀察者:

- (void)registerAsObserver {
    /*
     Register 'inspector' to receive change notifications for the "openingBalance" property of
     the 'account' object and specify that both the old and new values of "openingBalance"
     should be provided in the observe… method.
     */
    [account addObserver:inspector
             forKeyPath:@"openingBalance"
                 options:(NSKeyValueObservingOptionNew |
                            NSKeyValueObservingOptionOld)
                    context:NULL];
}

inspector注冊成為了account的觀察者,被觀察屬性的KeyPath是@"openingBalance",也就是account的openingBalance屬性,NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld選項分別標識在觀察者接收通知時change字典對應入口提供更改后的值和更改前的值。更簡單的辦法是用 NSKeyValueObservingOptionPrior選項,隨后我們就可以用以下方式提取出改變前后的值:(change是個字典,詳細介紹請看下節)

id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];

我們常常需要當一個值改變的時候更新 UI,但是我們也要在第一次運行代碼的時候更新一次 UI。我們可以用 KVO 并添加 NSKeyValueObservingOptionInitial的選項 來一箭雙雕地做好這樣的事情。這將會讓 KVO 通知在調用-addObserver:forKeyPath:...到時候也被觸發。

當我們注冊 KVO 通知的時候,我們可以添加 NSKeyValueObservingOptionPrior 選項,這能使我們在鍵值改變之前被通知。這和-willChangeValueForKey:被觸發的時間相對應。

如果我們注冊通知的時候附加了NSKeyValueObservingOptionPrior選項,我們將會收到兩個通知:一個在值變更前,另一個在變更之后。變更前的通知將會在 change 字典中有不同的鍵。

context是一個指針,當observeValueForKeyPath: ofObject: change: context:方法執行時context會提供給觀察者。context可以是C指針或者一個對象引用,既可以當作一個唯一的標識來分辨被觀察的變更,也可以向觀察者提供數據。

接收變更通知

當被觀察的屬性變更時,觀察者會接到observeValueForKeyPath: ofObject: change: context:消息,所有的觀察者都必須實現這個方法。
觀察者會被提供觸發通知的對象和keyPath,一個包含變更詳細信息的字典,還有一個注冊觀察者時提供的context指針。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context 
{
    if ([keyPath isEqual:@"openingBalance"]) 
    {
        [openingBalanceInspectorField setObjectValue:
            [change objectForKey:NSKeyValueChangeNewKey]];
    }
    /*
     Be sure to call the superclass's implementation *if it implements it*.
     NSObject does not implement the method.
     */
    [super observeValueForKeyPath:keyPath
                         ofObject:object
                           change:change
                           context:context];
}

關于change參數,它是一個字典,有五個常量作為它的鍵:

NSString *const NSKeyValueChangeKindKey;  
NSString *const NSKeyValueChangeNewKey;  
NSString *const NSKeyValueChangeOldKey;  
NSString *const NSKeyValueChangeIndexesKey;  
NSString *const NSKeyValueChangeNotificationIsPriorKey;
  • NSKeyValueChangeKindKey
    指明了變更的類型,值為“NSKeyValueChange”枚舉中的某一個,類型為NSNumber。
enum {
   NSKeyValueChangeSetting = 1,
   NSKeyValueChangeInsertion = 2,
   NSKeyValueChangeRemoval = 3,
   NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
  • NSKeyValueChangeNewKey
    如果NSKeyValueChangeKindKey的值為 NSKeyValueChangeSetting,并且 NSKeyValueObservingOptionNew選項在注冊觀察者時也指定了,那么這個鍵的值就是屬性變更后的新值。
    對于 NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,如果 NSKeyValueObservingOptionNew選項在注冊觀察者時也指定了,這個鍵的值是一個數組,其包含了插入或替換的對象。

  • NSKeyValueChangeOldKey
    如果 NSKeyValueChangeKindKey的值為 NSKeyValueChangeSetting,并且NSKeyValueObservingOptionOld選項在注冊觀察者時也指定了,那么這個鍵的值就是屬性變更前的舊值。
    對于 NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement,如果 NSKeyValueObservingOptionOld選項在注冊觀察者時也指定了,這個鍵的值是一個數組,其包含了被移除或替換的對象。

  • NSKeyValueChangeIndexesKey
    如果 NSKeyValueChangeKindKey的值為NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,這個鍵的值是一個NSIndexSet對象,包含了增加,移除或者替換對象的index。

  • NSKeyValueChangeNotificationIsPriorKey
    如果注冊觀察者時NSKeyValueObservingOptionPrior選項被指明了,此通知會在變更發生前被發出。其類型為NSNumber,包含的值為YES。我們可以像以下這樣區分通知是在改變之前還是之后被觸發的:

if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
    // 改變之前
} else {
    // 改變之后
}

移除觀察者

你可以通過發送removeObserver: forKeyPath: 消息來移除觀察者,你需要指明觀察對象和路徑。

- (void)unregisterForChangeNotification {
    [observedObject removeObserver:inspector forKeyPath:@"openingBalance"];
}

上面的代碼將openingBalance屬性的觀察者inspector移除,移除后觀察者再也不會收到observeValueForKeyPath: ofObject: change: context:消息。
在移除觀察者之前,如果context是一個對象的引用,那么必須保持對它的強引用直到觀察者被移除。

KVO Compliance(KVO兼容)

有兩種方法可以保證變更通知被發出。自動發送通知是NSObject提供的,并且一個類中的所有屬性都默認支持,只要是符合KVO的。一般情況你使用自動變更通知,你不需要寫任何代碼。
人工變更通知需要些額外的代碼,但也對通知發送提供了額外的控制。你可以通過重寫子類automaticallyNotifiesObserversForKey:方法的方式控制子類一些屬性的自動通知。

Automatic Change Notification(自動通知)

下面代碼中的方法都能導致KVO變更消息發出

// Call the accessor method.
[account setName:@"Savings"];
 
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
 
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

Manual Change Notification(手動通知)

下面的代碼為openingBalance屬性開啟了人工通知,并讓父類決定其他屬性的通知方式。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"openingBalance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

要實現人工觀察者通知,你要執行在變更前執行willChangeValueForKey:方法,在變更后執行didChangeValueForKey:方法:

- (void)setOpeningBalance:(double)theBalance {
    [self willChangeValueForKey:@"openingBalance"];
    _openingBalance = theBalance;
    [self didChangeValueForKey:@"openingBalance"];
}

為了使不必要的通知最小化我們應該在變更前先檢查一下值是否變了:

- (void)setOpeningBalance:(double)theBalance {
    if (theBalance != _openingBalance) {
        [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"];
    }
}

如果一個操作導致了多個鍵的變化,你必須嵌套變更通知:

- (void)setOpeningBalance:(double)theBalance {
    [self willChangeValueForKey:@"openingBalance"];
    [self willChangeValueForKey:@"itemChanged"];
    _openingBalance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"openingBalance"];
}

在to-many關系操作的情形中,你不僅必須表明key是什么,還要表明變更類型和影響到的索引。變更類型是一個 NSKeyValueChange值,被影響對象的索引是一個 NSIndexSet對象。
下面的代碼示范了在to-many關系transactions對象中的刪除操作:

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}

Registering Dependent Keys(注冊依賴鍵)

有一些屬性的值取決于一個或者多個其他對象的屬性值,一旦某個被依賴的屬性值變了,依賴它的屬性的變化也需要被通知。

To-one Relationships

要自動觸發to-one關系,有兩種方法:
重寫keyPathsForValuesAffectingValueForKey:方法 或者
定義名稱為keyPathsForValuesAffecting<Key>的方法。
例如一個人的全名是由姓氏和名子組成的:

- (NSString *)fullName
{
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

一個觀察fullName的程序在firstName或者lastName變化時也應該接收到通知。
一種解決方法是重寫keyPathsForValuesAffectingValueForKey:方法來表明fullname屬性是依賴于firstname和lastname的:

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

相當于在影響fullName值的keypath中新加了兩個key:lastName和firstName,很容易理解。

另一種實現同樣結果的方法是實現一個遵循命名方式為keyPathsForValuesAffecting<Key>的類方法,<Key>是依賴于其他值的屬性名(首字母大寫),用上面代碼的例子來重新實現一下:

+ (NSSet *)keyPathsForValuesAffectingFullName
{
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

有時在類別中我們不能添加keyPathsForValuesAffectingValueForKey:方法,因為不能再類別中重寫方法,所以這時可以實現keyPathsForValuesAffecting<Key>方法來代替。
注意:你不能在keyPathsForValuesAffectingValueForKey:方法中設立to-many關系的依賴,相反,你必須觀察在to-many集合中的每一個對象中相關的屬性并通過親自更新他們的依賴來回應變更。下一節將會講述對付此情形的策略。

To-many Relationships

keyPathsForValuesAffectingValueForKey:方法不支持包含to-many關系的keypath。比如,假如你有一個Department類,它有一個針對Employee類的to-many關系(雇員),Employee類有salary屬性。你希望Department類有一個totalSalary屬性來計算所有員工的薪水,也就是在這個關系中Department的totalSalary依賴于所有Employee的salary屬性。你不能通過實現keyPathsForValuesAffectingTotalSalary方法并返回employees.salary。

有兩種解決方法:
你可以用KVO將parent(比如Department)作為所有children(比如Employee)相關屬性的觀察者。你必須在把child添加或刪除到parent時也把parent作為child的觀察者添加或刪除。在observeValueForKeyPath:ofObject:change:context:方法中我們可以針對被依賴項的變更來更新依賴項的值:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

2.如果你在使用Core Data,你可以在應用的notification center中將parent注冊為它的 managed object context的觀察者,parent應該回應相應的變更通知,這些通知是children以類似KVO的形式發出的。
其實這也是Objective-C中利用Cocoa實現觀察者模式的另一種途徑:NSNotificationCenter.

調試KVO

你可以在 lldb 里查看一個被觀察對象的所有觀察信息。

(lldb) po [observedObject observationInfo]

這會打印出有關誰觀察誰之類的很多信息。
這個信息的格式不是公開的,我們不能讓任何東西依賴它,因為蘋果隨時都可以改變它。不過這是一個很強大的排錯工具。

KVO優點

首先,不需要自己去實現這樣的方案,這個獲得框架級支持,可以方便地采用。不需要設計自己的觀察者模型,直接可以在工程里使用。
其次,KVO的架構非常的強大,可以很容易的支持多個觀察者觀察同一個屬性,以及相關的值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容