在iOS開發(fā)過程中,我們經常會聽到或者用到KVO/KVC,但是對于什么是KVO和KVC,我們可能沒有那么了解。下面先讓我們來了解一下什么是KVC.
什么是KVO
在蘋果的官方文檔中是這樣描述KVC的:它是一種通過字符串描述符而不是通過調用訪問方法或者直接使用實例變量的非直接的訪問對象屬性的機制,說白了就是KVO是一種通過非常規(guī)方法訪問成員變量或者屬性的機制,這種非常規(guī)方式就是通過一個字符串標示符也就是所謂的key來訪問屬性或者成員變量。而這個key一般就是屬性名或者實例變量名。
對于KVC的基本的方法都定義在NSKeyValueCoding的非正式協(xié)議中,并且NSObject默認實現了該協(xié)議。
KVC不僅支持對象類型,也支持數值類型和結構體。非對象類型的參數和返回類型會自動封裝成NSValue或NSNumber類型。
KVO可以用來訪問三種不同的對象值類型:屬性、一對一關系、一對多關系
屬性可以是諸如數值、字符串、bool類型等簡單的值。也可以NSNumber或者NSColor這樣的對象值。
在一對一關系里的對象可以擁有它自己的屬性,這些屬性可以在不改變對象的情況下被改變。像UIView的superView的屬性,我們可以更改superView的屬性,而不需要更改UIView。
一對多屬性是一些相關對象的集合。通常用NSArray或者NSSet來存儲這些集合。KVO也允許用戶自定義集合類,但依然是像訪問NSArray或者NSSet一樣訪問它們。
鍵和鍵路徑
鍵是用來標識一個對象屬性的字符串。一般情況下,鍵就是訪問方法或者是對象的實例變量的名字。鍵必須是ASCII編碼,以小寫字母開頭,并且不能包含空格。舉幾個鍵的例子:age、firstName、lastNmame等。
鍵路徑是一串由點分隔的鍵組成的字符串,它是用來遍歷一系列的對象屬性的。第一個鍵的屬性是跟接收者相關的,而每一個子系列是跟前一個屬性相關的。比如鍵路徑address.street,這個鍵路徑會首先從接收者獲得address屬性,然后從address屬性中獲得street屬性。
用KVC獲得屬性的值
方法valueForKey:會返回跟接收者相關的key的值。如果對于指定的key沒有訪問器或者實例變量,則給自己發(fā)送一個valueForUndefinedKey:消息,這個方法的默認實現是拋出一個NSUndefinedKeyException。子類可以重寫這個方法。
同樣的,valueForKeyPath:返回跟接收者相關的鍵路徑的值。對于子系列中任何不遵循KVC的對象,都會收到一個valueForUndefineKey:消息。
dictionaryWithValuesForKeys:會檢索數組中所有跟接收者相關的key的值。返回的NSDictionary中包含了數組中所有key的值。
注意:集合對象,比如NSArray、NSSet和NSDictionary中不能將nil作為一個值。相反的,應該用NSNull對象代替nil。NSNull是一個代表nil的對象屬性。dictionaryWithValuesForKeys:和setValuesForKeysWithDictionary:方法的實現中,默認會在nil和NSNull之間進行轉換。在你的對象中,不需要對nil做顯示的測試。
用KVC設置屬性的值
方法setValue:forKey:是將接收者中相關key的值設置成指定的值。在這個方法的實現中,會將NSValue的值轉換成普通的數值然后賦給屬性。
如果指定的key不存在,會給接收者發(fā)送一個setValue:forUndefinedKey:消息。這個方法的默認實現是拋出一個NSUndefinedKeyException異常,子類可以重寫這個方法來自定義默認行為。
方法setValue:forKeyPath:的實現跟前面的一樣,只不過它是用來處理鍵路徑的。
setValuesForKeysWithDictionary:方法是用指定字典里的值來賦給接收者相關的屬性。這個方法的默認實現是對每一對鍵-值都調用一次setValue:forKey:方法,并且自動將nil轉成NSNull。
最后,你要關心的當嘗試將一個nil值賦給一個非對象類型的時候該怎么辦。這種情況下,接收者會發(fā)出一個setNilValueForKey:消息,這個方法的默認實現是拋出一個NSInvalidArgumentException。在你的應用中可以重寫這個方法來定義一個默認值,然后用新的值觸發(fā)setValue:forKey:。
在Cocoa中,NSObject默認實現了NSKeyValueCoding協(xié)議,也就是說,我們不需要自己再去實現NSKeyValueCoding協(xié)議,這極大的方便了我們的編程。試用了KVC之后,我們不僅可以通過訪問方法來設置和修改屬性的值,也可以通過NSKeyValueCoding提供的方法setVAlue:forKey和valueForKey:來設置和獲得屬性的值。
接下來,讓我們先頂一個一個Person類:
[cpp] view plaincopy
- @interface Person : NSObject {
- NSString *_firstName;
- NSString *lastName;
- BOOL married;
- }
-
- @property (nonatomic, strong) NSString *address;
- @property (nonatomic, assign) NSInteger age;
-
- @end
在Person類中,我們定義了三個成員變量和兩個屬性。一般情況下,我們只能通過成員變量名訪問成員變量,并且只能是Person類內部使用,外部的類是無法訪問Person的成員變量,對于屬性,無論是在類內還是在類外都可以通過屬性的set和get方法進行訪問,下面我們通過KVC的方法來設置Person類屬性和成員變量的值。
[cpp] view plaincopy
- Person *person = [[Person alloc] init];
- person.address = @"北京市朝陽區(qū)";
- [person setValue:[NSNumber numberWithBool:YES] forKey:@"married"];
- [person setValue:[NSNumber numberWithInt:26] forKey:@"age"];
- [person setValue:@"傾城" forKey:@"firstName"];
- [person setValue:@"呂" forKey:@"lastName"];
除了address屬性之外,其他屬性(或成員變量)的值都是通過KVC提供的方法來設置的。接下來,我們將屬性(或成員變量)的值讀出來看看是不是與我們設置的值是一樣的。對于使用KVC方法設置的值,我們不通過KVC的方法來讀取,而是使用普通的方法,對于普通方法設置的值,我們通過KVC提供的方法來讀取.
[cpp] view plaincopy
- [person valueForKey:@"address"]);
- NSLog(@"Age:%d",person.age);
-
- NSLog(@"Married:%d",[person isMarried]);
- NSLog(@"First Name: %@", [person firstName]);
- NSLog(@"Last Name: %@", [person lastName]);
輸出結果為:
[cpp] view plaincopy
- 2013-08-08 11:43:04.810 TestKVC[2978:c07] Address:北京市朝陽區(qū)
- 2013-08-08 11:43:04.812 TestKVC[2978:c07] Age:26
- 2013-08-08 11:43:04.812 TestKVC[2978:c07] Married:1
- 2013-08-08 11:43:04.813 TestKVC[2978:c07] First Name: 傾城
- 2013-08-08 11:43:04.813 TestKVC[2978:c07] Last Name: 呂
與我們之前的設置一樣。
對于成員變量,我們不能使用屬性的get方法來訪問,必須自己實現成員變量的訪問方法。在這里,我們也可以使用KVC提供的valueForKey:方法來訪問成員變量,這樣我們就不需要自己實現成員變量的set和get方法了。
在上面的例子中,我們既有屬性,又有成員變量。成員變量的命名有帶下劃線的,有不帶下劃線的。那么KVC是如何來查找這些東西的呢。
setValue:forKey:的搜索模式
- 如果沒有并且接收者類的accessInstanceVariableDirectly的方法返回YES,接下來就要搜索成員變量了,順序是:_<key>,_is<key>,<key>,is<key>
- 如果都沒有找到,接收者類的setValue:forUndefinedKey:方法將被調用
valueForKey:的搜索模式
- 按get<Key>,<key>,is<Key>的順序搜索名字相符的訪問方法,找到之后,相應的方法就會被調用。如果這個方法的返回類型是一個對象,那么結果將會被直接返回。如果返回的是NSNumber支持的數值類型,則將結果封裝成NSNumber進行返回。如果不是NSNumber支持的類型,則會將結果轉成NSValue進行返回(NSValue支持的類型包括NSPoint、NSRange、NSrect和NSSize等)
- 如果上面的方法都沒有找到,接下來會搜索接收者類中的countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes。如果countOf<Key>方法和后面兩個之一被找到的話,一個能響應所有NSArray方法的集合代理對象將會被返回。每一個發(fā)送給結合代理對象的NSArray消息都會使發(fā)送給接收者的valueForKey:的消息和countOf<Key>、objectIN<Key>AtIndex和<key>AtIndexes之間產生某種關聯(lián)。如果接收者類也實現了名字如get<Key>:range:的可選方法,為了提高性能,這個方法將會在合適的時候被調用。
- 如果簡單訪問方法和NSArray的方法都沒有被找到的話,接下來就會搜索接收者類中名字如下的一組方法:countOf<Key>,enumeratorOf<Key>和memberOf<Key>。如果這三個方法都被找到的話,一個能夠響應所有NSSet方法的集合代理對象會被返回。每一個發(fā)給集合代理對象的NSSet消息都會使接收者類的valuefForyKey:的消息和countOf<Key>,enumeratorOf<Key>和memberOf<Key>之間產生某種關聯(lián)。
- 如果上面的搜索都沒有結果并且接收者類的accessInstanceVariableDirectly方法返回YES,接收者類按_<key>,_is<Key>,<key>,is<Key>的順序搜索成員變量,如果找到了,成員變量的值將會被返回。如果成員變量的值類型是NSNumber支持的類型,將值進行轉換之后返回一個NSNumber對象。否則將轉換成NSValue對象進行返回。
- 如果還是什么都沒有找到的話,就會調用valueForUndefinedKey:
如果你不想要setValue:forUndefiedKey:和valueForUndefinedKey的默認實現,你就需要自己重寫這兩個方法。