Objective-C中的KVC和KVO

轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/05/12/objective-czhong-de-kvche-kvo/

本文講述了使用Cocoa框架中的KVC和KVO,實(shí)現(xiàn)觀察者模式

KVC

鍵/值編碼中的基本調(diào)用包括-valueForKey:和-setValue:forKey:。以字符串的形式向?qū)ο蟀l(fā)送消息,這個(gè)字符串是我們關(guān)注的屬性的關(guān)鍵。

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

對(duì)于KVC,Cocoa自動(dòng)放入和取出標(biāo)量值(int,float和struct)放入NSNumber或NSValue中;當(dāng)使用-setValue:ForKey:時(shí),它自動(dòng)將標(biāo)量值從這些對(duì)象中取出。僅KVC具有這種自動(dòng)包裝功能,常規(guī)方法調(diào)用和屬性語(yǔ)法不具備該功能。

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

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

最簡(jiǎn)單的 KVC 能讓我們通過(guò)以下的形式訪問屬性:

1

@property(nonatomic,copy)NSString*name;

取值:

1

NSString*n = [objectvalueForKey:@"name"];

設(shè)定:

1

[objectsetValue:@"Daniel"forKey:@"name"];

值得注意的是這個(gè)不僅可以訪問作為對(duì)象屬性,而且也能訪問一些標(biāo)量(例如int和CGFloat)和 struct(例如CGRect)。Foundation 框架會(huì)為我們自動(dòng)封裝它們。舉例來(lái)說(shuō),如果有以下屬性:

1

@property(nonatomic) CGFloat height;

我們可以這樣設(shè)置它:

1

[objectsetValue:@(20) forKey:@"height"];

有關(guān)KVC的更多用法,參看下面的文章:

http://blog.csdn.net/omegayy/article/details/7381301

http://blog.csdn.net/wzzvictory/article/details/9674431

http://objccn.io/issue-7-3/

KVO

KVO是Cocoa提供的一種稱為鍵-值觀察的機(jī)制,對(duì)象可以通過(guò)它得到其他對(duì)象特性屬性的變更通知。這種機(jī)制在MVC模式的場(chǎng)景中很重要,因?yàn)樗屢晥D對(duì)象可以經(jīng)由控制器層觀察模型對(duì)象的變更。

這一機(jī)制基于NSKeyValueObserving非正式協(xié)議,Cocoa通過(guò)這個(gè)協(xié)議為所有遵守協(xié)議的對(duì)象提供了一種自動(dòng)化的屬性觀察能力。要實(shí)現(xiàn)自動(dòng)觀察,參與KVO的對(duì)象需要符合KVC的要求和存取方法,也可以手動(dòng)實(shí)現(xiàn)觀察者通知,也可以兩者都保留。

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

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

首先看看你當(dāng)前的場(chǎng)景如果使用KVO是否更妥當(dāng),比如,當(dāng)一個(gè)實(shí)例的某個(gè)具體屬性有任何變更的時(shí)候,另一個(gè)實(shí)例需要被通知。

比如,BankObject中的accountBalance屬性有任何變更時(shí),某個(gè)PersonObject對(duì)象都要覺察到。

這個(gè)PersonObject對(duì)象必須注冊(cè)成為BankObject的accountBalance屬性的觀察者,可以通過(guò)發(fā)送addObserver:forKeyPath:options:context:消息來(lái)實(shí)現(xiàn)。

注意:addObserver:forKeyPath:options:context:方法在你指定的兩個(gè)實(shí)例間建立聯(lián)系,而不是在兩個(gè)類之間。

為了回應(yīng)變更通知,觀察者必須實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法。這個(gè)方法的實(shí)現(xiàn)決定了觀察者如何回應(yīng)變更通知。你可以在這個(gè)方法里自定義如何回應(yīng)被觀察屬性的變更。

當(dāng)一個(gè)被觀察屬性的值以符合KVO方式變更或者當(dāng)它依賴的鍵變更時(shí),observeValueForKeyPath:ofObject:change:context:方法會(huì)被自動(dòng)執(zhí)行。

Registering for Key-Value Observing

注冊(cè)成為觀察者

你可以通過(guò)發(fā)送addObserver:forKeyPath:options:context:消息來(lái)注冊(cè)觀察者

1

2

3

4

5

6

7

8

9

10

11

12

- (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注冊(cè)成為了account的觀察者,被觀察屬性的KeyPath是@"openingBalance",也就是account的openingBalance屬性,NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld選項(xiàng)分別標(biāo)識(shí)在觀察者接收通知時(shí)change字典對(duì)應(yīng)入口提供更改后的值和更改前的值。更簡(jiǎn)單的辦法是用NSKeyValueObservingOptionPrior選項(xiàng),隨后我們就可以用以下方式提取出改變前后的值:(change是個(gè)字典,詳細(xì)介紹請(qǐng)看下節(jié))

1

2

idoldValue = change[NSKeyValueChangeOldKey];

idnewValue = change[NSKeyValueChangeNewKey];

我們常常需要當(dāng)一個(gè)值改變的時(shí)候更新 UI,但是我們也要在第一次運(yùn)行代碼的時(shí)候更新一次 UI。我們可以用 KVO 并添加NSKeyValueObservingOptionInitial的選項(xiàng) 來(lái)一箭雙雕地做好這樣的事情。這將會(huì)讓 KVO 通知在調(diào)用-addObserver:forKeyPath:...到時(shí)候也被觸發(fā)。

當(dāng)我們注冊(cè) KVO 通知的時(shí)候,我們可以添加NSKeyValueObservingOptionPrior選項(xiàng),這能使我們?cè)阪I值改變之前被通知。這和-willChangeValueForKey:被觸發(fā)的時(shí)間相對(duì)應(yīng)。

如果我們注冊(cè)通知的時(shí)候附加了NSKeyValueObservingOptionPrior選項(xiàng),我們將會(huì)收到兩個(gè)通知:一個(gè)在值變更前,另一個(gè)在變更之后。變更前的通知將會(huì)在change字典中有不同的鍵。

context是一個(gè)指針,當(dāng)observeValueForKeyPath:ofObject:change:context:方法執(zhí)行時(shí)context會(huì)提供給觀察者。context可以是C指針或者一個(gè)對(duì)象引用,既可以當(dāng)作一個(gè)唯一的標(biāo)識(shí)來(lái)分辨被觀察的變更,也可以向觀察者提供數(shù)據(jù)。

接收變更通知

當(dāng)被觀察的屬性變更時(shí),觀察者會(huì)接到observeValueForKeyPath:ofObject:change:context:消息,所有的觀察者都必須實(shí)現(xiàn)這個(gè)方法。

觀察者會(huì)被提供觸發(fā)通知的對(duì)象和keyPath,一個(gè)包含變更詳細(xì)信息的字典,還有一個(gè)注冊(cè)觀察者時(shí)提供的context指針。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

-(void)observeValueForKeyPath:(NSString*)keyPath

ofObject:(id)object

change:(NSDictionary*)change

context:(void*)context{

if([keyPathisEqual:@"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];

}

關(guān)于change參數(shù),它是一個(gè)字典,有五個(gè)常量作為它的鍵:

1

2

3

4

5

NSString*constNSKeyValueChangeKindKey;

NSString*constNSKeyValueChangeNewKey;

NSString*constNSKeyValueChangeOldKey;

NSString*constNSKeyValueChangeIndexesKey;

NSString*constNSKeyValueChangeNotificationIsPriorKey;

NSKeyValueChangeKindKey

指明了變更的類型,值為“NSKeyValueChange”枚舉中的某一個(gè),類型為NSNumber。

1

2

3

4

5

6

7

enum{

NSKeyValueChangeSetting =1,

NSKeyValueChangeInsertion =2,

NSKeyValueChangeRemoval =3,

NSKeyValueChangeReplacement =4

};

typedef NSUInteger NSKeyValueChange;

NSKeyValueChangeNewKey

如果NSKeyValueChangeKindKey的值為NSKeyValueChangeSetting,并且NSKeyValueObservingOptionNew選項(xiàng)在注冊(cè)觀察者時(shí)也指定了,那么這個(gè)鍵的值就是屬性變更后的新值。

對(duì)于NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,如果NSKeyValueObservingOptionNew選項(xiàng)在注冊(cè)觀察者時(shí)也指定了,這個(gè)鍵的值是一個(gè)數(shù)組,其包含了插入或替換的對(duì)象。

NSKeyValueChangeOldKey

如果NSKeyValueChangeKindKey的值為NSKeyValueChangeSetting,并且NSKeyValueObservingOptionOld選項(xiàng)在注冊(cè)觀察者時(shí)也指定了,那么這個(gè)鍵的值就是屬性變更前的舊值。

對(duì)于NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement,如果NSKeyValueObservingOptionOld選項(xiàng)在注冊(cè)觀察者時(shí)也指定了,這個(gè)鍵的值是一個(gè)數(shù)組,其包含了被移除或替換的對(duì)象。

NSKeyValueChangeIndexesKey

如果NSKeyValueChangeKindKey的值為NSKeyValueChangeInsertion,NSKeyValueChangeRemoval, 或者NSKeyValueChangeReplacement,這個(gè)鍵的值是一個(gè)NSIndexSet對(duì)象,包含了增加,移除或者替換對(duì)象的index。

NSKeyValueChangeNotificationIsPriorKey

如果注冊(cè)觀察者時(shí)NSKeyValueObservingOptionPrior選項(xiàng)被指明了,此通知會(huì)在變更發(fā)生前被發(fā)出。其類型為NSNumber,包含的值為YES。我們可以像以下這樣區(qū)分通知是在改變之前還是之后被觸發(fā)的:

1

2

3

4

5

if([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {

// 改變之前

}else{

// 改變之后

}

移除觀察者

你可以通過(guò)發(fā)送removeObserver:forKeyPath:消息來(lái)移除觀察者,你需要指明觀察對(duì)象和路徑。

1

2

3

- (void)unregisterForChangeNotification {

[observedObjectremoveObserver:inspectorforKeyPath:@"openingBalance"];

}

上面的代碼將openingBalance屬性的觀察者inspector移除,移除后觀察者再也不會(huì)收到observeValueForKeyPath:ofObject:change:context:消息。

在移除觀察者之前,如果context是一個(gè)對(duì)象的引用,那么必須保持對(duì)它的強(qiáng)引用直到觀察者被移除。

KVO Compliance(KVO兼容)

有兩種方法可以保證變更通知被發(fā)出。自動(dòng)發(fā)送通知是NSObject提供的,并且一個(gè)類中的所有屬性都默認(rèn)支持,只要是符合KVO的。一般情況你使用自動(dòng)變更通知,你不需要寫任何代碼。

人工變更通知需要些額外的代碼,但也對(duì)通知發(fā)送提供了額外的控制。你可以通過(guò)重寫子類automaticallyNotifiesObserversForKey:方法的方式控制子類一些屬性的自動(dòng)通知。

Automatic Change Notification(自動(dòng)通知)

下面代碼中的方法都能導(dǎo)致KVO變更消息發(fā)出

1

2

3

4

5

6

7

8

9

10

11

12

13

// Call the accessor method.

[accountsetName:@"Savings"];

// UsesetValue:forKey:.

[accountsetValue:@"Savings"forKey:@"name"];

// Use a key path,where'account'is a kvc-compliant property of'document'.

[documentsetValue:@"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(手動(dòng)通知)

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

1

2

3

4

5

6

7

8

9

10

11

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString*)theKey {

BOOLautomatic =NO;

if([theKey isEqualToString:@"openingBalance"]) {

automatic =NO;

}

else{

automatic = [superautomaticallyNotifiesObserversForKey:theKey];

}

returnautomatic;

}

要實(shí)現(xiàn)人工觀察者通知,你要執(zhí)行在變更前執(zhí)行willChangeValueForKey:方法,在變更后執(zhí)行didChangeValueForKey:方法:

1

2

3

4

5

- (void)setOpeningBalance:(double)theBalance {

[selfwillChangeValueForKey:@"openingBalance"];

_openingBalance = theBalance;

[selfdidChangeValueForKey:@"openingBalance"];

}

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

1

2

3

4

5

6

7

- (void)setOpeningBalance:(double)theBalance {

if(theBalance != _openingBalance) {

[selfwillChangeValueForKey:@"openingBalance"];

_openingBalance = theBalance;

[selfdidChangeValueForKey:@"openingBalance"];

}

}

如果一個(gè)操作導(dǎo)致了多個(gè)鍵的變化,你必須嵌套變更通知:

1

2

3

4

5

6

7

8

- (void)setOpeningBalance:(double)theBalance {

[selfwillChangeValueForKey:@"openingBalance"];

[selfwillChangeValueForKey:@"itemChanged"];

_openingBalance = theBalance;

_itemChanged = _itemChanged+1;

[selfdidChangeValueForKey:@"itemChanged"];

[selfdidChangeValueForKey:@"openingBalance"];

}

在to-many關(guān)系操作的情形中,你不僅必須表明key是什么,還要表明變更類型和影響到的索引。變更類型是一個(gè)NSKeyValueChange值,被影響對(duì)象的索引是一個(gè)NSIndexSet對(duì)象。

下面的代碼示范了在to-many關(guān)系transactions對(duì)象中的刪除操作:

1

2

3

4

5

6

7

8

9

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {

[selfwillChange:NSKeyValueChangeRemoval

valuesAtIndexes:indexesforKey:@"transactions"];

// Remove the transaction objects at the specified indexes.

[selfdidChange:NSKeyValueChangeRemoval

valuesAtIndexes:indexesforKey:@"transactions"];

}

Registering Dependent Keys(注冊(cè)依賴鍵)

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

To-one Relationships

要自動(dòng)觸發(fā) to-one關(guān)系,有兩種方法:重寫keyPathsForValuesAffectingValueForKey:方法或者定義名稱為keyPathsForValuesAffecting的方法。

例如一個(gè)人的全名是由姓氏和名子組成的:

1

2

3

-(NSString *)fullName{

return[NSString stringWithFormat:@"%@ %@",firstName, lastName];

}

一個(gè)觀察fullName的程序在firstName或者lastName變化時(shí)也應(yīng)該接收到通知。

一種解決方法是重寫keyPathsForValuesAffectingValueForKey:方法來(lái)表明fullname屬性是依賴于firstname和lastname的:

1

2

3

4

5

6

7

8

9

10

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

NSSet *keyPaths = [superkeyPathsForValuesAffectingValueForKey:key];

if([keyisEqualToString:@"fullName"]) {

NSArray *affectingKeys = @[@"lastName", @"firstName"];

keyPaths = [keyPathssetByAddingObjectsFromArray:affectingKeys];

}

returnkeyPaths;

}

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

另一種實(shí)現(xiàn)同樣結(jié)果的方法是實(shí)現(xiàn)一個(gè)遵循命名方式為keyPathsForValuesAffecting的類方法,是依賴于其他值的屬性名(首字母大寫),用上面代碼的例子來(lái)重新實(shí)現(xiàn)一下:

1

2

3

+ (NSSet*)keyPathsForValuesAffectingFullName {

return[NSSetsetWithObjects:@"lastName",@"firstName",nil];

}

有時(shí)在類別中我們不能添加keyPathsForValuesAffectingValueForKey:方法,因?yàn)椴荒茉兕悇e中重寫方法,所以這時(shí)可以實(shí)現(xiàn)keyPathsForValuesAffecting方法來(lái)代替。

注意:你不能在keyPathsForValuesAffectingValueForKey:方法中設(shè)立to-many關(guān)系的依賴,相反,你必須觀察在to-many集合中的每一個(gè)對(duì)象中相關(guān)的屬性并通過(guò)親自更新他們的依賴來(lái)回應(yīng)變更。下一節(jié)將會(huì)講述對(duì)付此情形的策略。

To-many Relationships

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

有兩種解決方法:

你可以用KVO將parent(比如Department)作為所有children(比如Employee)相關(guān)屬性的觀察者。你必須在把child添加或刪除到parent時(shí)也把parent作為child的觀察者添加或刪除。在observeValueForKeyPath:ofObject:change:context:方法中我們可以針對(duì)被依賴項(xiàng)的變更來(lái)更新依賴項(xiàng)的值:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

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

if(context == totalSalaryContext) {

[selfupdateTotalSalary];

}

else

// deal with other observations and/or invoke super...

}

- (void)updateTotalSalary {

[selfsetTotalSalary:[selfvalueForKeyPath:@"employees.@sum.salary"]];

}

- (void)setTotalSalary:(NSNumber*)newTotalSalary {

if(totalSalary != newTotalSalary) {

[selfwillChangeValueForKey:@"totalSalary"];

_totalSalary = newTotalSalary;

[selfdidChangeValueForKey:@"totalSalary"];

}

}

- (NSNumber*)totalSalary {

return_totalSalary;

}

2.如果你在使用Core Data,你可以在應(yīng)用的notification center中將parent注冊(cè)為它的 managed object context的觀察者,parent應(yīng)該回應(yīng)相應(yīng)的變更通知,這些通知是children以類似KVO的形式發(fā)出的。

其實(shí)這也是Objective-C中利用Cocoa實(shí)現(xiàn)觀察者模式的另一種途徑:NSNotificationCenter

調(diào)試KVO

你可以在 lldb 里查看一個(gè)被觀察對(duì)象的所有觀察信息。

1

(lldb) po[observedObject observationInfo]

這會(huì)打印出有關(guān)誰(shuí)觀察誰(shuí)之類的很多信息。

這個(gè)信息的格式不是公開的,我們不能讓任何東西依賴它,因?yàn)樘O果隨時(shí)都可以改變它。不過(guò)這是一個(gè)很強(qiáng)大的排錯(cuò)工具。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 本文講述了使用Cocoa框架中的KVC和KVO,實(shí)現(xiàn)觀察者模式 鍵/值編碼中的基本調(diào)用包括-valueForKey...
    茗涙閱讀 713評(píng)論 0 3
  • 本文轉(zhuǎn)自:Objective-C中的KVC和KVO. KVC KVO2.1. Registering for Ke...
    0o凍僵的企鵝o0閱讀 427評(píng)論 0 3
  • 本文講述了使用Cocoa框架中的KVC和KVO,實(shí)現(xiàn)觀察者模式 KVC 鍵/值編碼中的基本調(diào)用包括-valueFo...
    voQuan閱讀 338評(píng)論 0 1
  • 在編程中,最常見的就是程序的流程取決于你所使用的各種變量和屬性的值,根據(jù)變量和屬性的值確定后面運(yùn)行的代碼,有時(shí)會(huì)檢...
    pro648閱讀 1,658評(píng)論 2 27
  • 我要在宇宙找一顆星星為你命名, 在你不在的日子 傾訴或祝福
    慕木穆閱讀 106評(píng)論 0 0