KVC又稱鍵值編碼 (Key-Value-Coding)
,在iOS開發中是一個比較常見的技術點,相信不少開發人員都使用過KVC,其主要的兩個方法就是以下兩個,分別對應設置值和取值:html
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;
複製代碼
平時的開發中,咱們有不少地方使用到KVC,例如給一個對象的屬性賦值、StroyBoard中給控件的layer設置邊框等都是KVC的應用,本篇文章就來探索下KVC的原理及使用注意事項。git
在代碼中調用KVC,而後進入源碼定義處,能夠發現蘋果給 NSObject、NSArray等都加了一個# NSKeyValueCoding
分類,以下圖所示:github
NSKeyValueCoding
是什麼呢?經過 KVC文檔 和 NSKeyValueCoding 能夠發現其解釋以下:數組
圖中的大體意思是,NSKeyValueCoding
是一種非正式協議,它提供了一種能夠間接訪問對象屬性的機制,這套機制使得咱們能夠經過一個簡潔明晰的字符串接口訪問對象屬性。這種機制補充了對象的實例變量的訪問。安全
怎麼理解這段話呢?咱們新建一個工程 KVCDemo,而後建立一個 BPPerson
類,在該類中分別添加屬性及成員變量以下:markdown
在調用bpName、title、hobby
時,會出現以下狀況:網絡
圖中的錯誤咱們均可以理解,bpName 定義在.m實現文件中,外部沒法直接訪問,其實若是 hobby
不添加 @public
修飾,其默認爲 protect
,也是沒法訪問的。可是經過 KVC
則能夠避免這些限制,代碼以下:多線程
void personKVC(void) {
BPPerson *person = [BPPerson alloc];
// person.title = @"攻城獅";
// person->hobby = @"running and programming";
// person->bpName = @"奔跑";
[person setValue:@"BP" forKey:@"bpName"];
[person setValue:@"running and programming" forKey:@"hobby"];
[person setValue:@"攻城獅" forKey:@"title"];
[person printPerson];
[person printPersonKVC];
}
複製代碼
代碼運行結果以下app
能夠發現經過KVC成功訪問了屬性和成員變量,而且成功賦值和取值。ide
KVC中最爲基礎的兩個 API就是 setValue: forKey: 和 valueForKey:,分別是根據 Key 設置值 和取出值,其用法如上面例子所示。此外,還有一些其餘的方法供咱們調用,下面以以下幾個方法爲例進行探究「
第一個方法以下:
/* Given a key that identifies an _ordered_ to-many relationship, return a mutable array that provides read-write access to the related objects. Objects added to the mutable array will become related to the receiver, and objects removed from the mutable array will become unrelated. */
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
複製代碼
在對象包含一個數據變量時,若是調用該方法,會獲得一個可變數組,經過修改該數組中的元素,使得原數組也發生變動,即便原數組是不可變數組。下面咱們驗證下,在 BPPerson
中添加一個不可變數組屬性
**@property** (**nonatomic**, **copy**) NSArray *array;
複製代碼
進行以下調用,並獲得的結果以下:
能夠發現原數組原本是 @[@"1", @"2", @"3"],原本不能夠被修改,可是經過調用該方法,獲得可變數組,並修改其中的值後,發現原數組也變爲了圖中結果。
第二個方法主要是爲了介紹 keyPath,與之相關的有讀寫兩個,因此一塊兒介紹:
- (**nullable** **id**)valueForKeyPath:(NSString *)keyPath;
- (**void**)setValue:(**nullable** **id**)value forKeyPath:(NSString *)keyPath;
複製代碼
咱們先新建一個 BPAnimal 類,而後在 BPPerson 中加一個 pet 屬性,代碼以下:
**@interface** BPPerson : NSObject {
**@public** NSString *hobby;
}
**@property** (**nonatomic**, **copy**) NSString *title;
**@property** (**nonatomic**, **copy**) NSArray *array;
**@property** (**nonatomic**, **strong**) BPAnimal *pet; // 寵物
- (**void**)printPerson;
- (**void**)printPersonKVC;
**@end**
**@interface** BPAnimal : NSObject
**@property** (**nonatomic**, **copy**) NSString *breed; // 品種
**@end**
複製代碼
BPPerson 中添加一個寵物 pet 屬性, 寵物有本身的品種,所以 BPAnimal 中包含一個 breed 屬性。 下面經過keyPath來嘗試寫入和取值,代碼和結果以下
由圖中結果能夠看出,經過 pet.breed 這一條 keyPath,成功完成賦值。此外還有其餘的API還沒探索,有興趣的小夥伴能夠繼續探索,這裏探索了平時使用較多的API。
在平時咱們使用 NSArray 時,用到最多的就是 objectAtIndex,根據下標進行取值。可是在查看KVC的定義時能夠發現,NSArray 和 NSMutableArray 也有 valueForKey 的方法,以下代碼所示:
**@interface** NSArray<ObjectType>(NSKeyValueCoding)
/* Return an array containing the results of invoking -valueForKey: on each of the receiver's elements. The returned array will contain NSNull elements for each instance of -valueForKey: returning nil.*/
- (**id**)valueForKey:(NSString *)key;
/* Invoke -setValue:forKey: on each of the receiver's elements.*/
- (**void**)setValue:(**nullable** **id**)value forKey:(NSString *)key;
@end
複製代碼
經過註釋能夠得知,數組的 valueForKey 作的事情是,對數組中的元素調用 valueForKey 方法,並返回一個數組。經過如下demo能夠驗證這一點:
由結果發現,對於array調用 setValue:forKey:將原有的獅子改成了老虎,而取值也是統一取出了老虎,由於此時數組中的元素都發生了改變。
須要注意的是,若是數組中的元素不是同一個類的,那麼調用該方法就存在崩潰的風險,由於可能有部分元素不存在傳入的key,示例以下圖:
當最後一個元素換爲 person時,person沒有breed變量,所以發生崩潰,並報錯 setValue:forUndefinedKey:
在使用可變字典設置值時,咱們常常調用的是 setObjcet:forKey:,可是若是調用 setValue:forKey: 也不會報錯,而且也能成功賦值,那麼這兩個方法到底有什麼區別呢?咱們先看一下字典的KVC定義,代碼以下:
經過註釋能夠發現,在調用 setValue:forKey: 時,至關於調用了 setObjcet:forKey:,可是若是傳入的value爲nil,則會調用 -removeObjectForKey:,而取值調用 valueForKey: 則至關於調用 -objectForKey:。下面經過一個demo驗證下:
能夠發現,本來 BP 值爲 NP,第一次傳入 666,修改爲功,可是當第二次傳入 nil 時,直接移除了 BP 這個 Key。這裏還能夠發現一個細節,以後可變字典纔有 setValue:forKey:,而不可變字典中則沒有進行聲明,說明KVC也是仍是要保留了字典可變與不可變的特性。
對於KVC的調用流程,在 KVC Fundamentals 中能夠看到對應的描述,下面咱們分爲 setValue 和 valueForKey 兩部分來分析下其調用流程。
在官方文檔中能夠發現以下一段描述:
總結一下能夠分爲3步:
set<<Key>Key> 、 _set<Key>
的順序查找方法,若是找到了就調用並賦值,完成KVC流程accessInstanceVariablesDirectly
方法返回 YES,則按照 _<Key>、_is<Key>、<Key>、is<Key>
的順序查找,若是找到其中一個就將傳入的值賦值,並完成KVC流程setValue:forUndefinedKey:
方法,並拋出異常。下面經過一個Demo來驗證一下,首先按照規則定義對應的成員變量和方法,並先將accessInstanceVariablesDirectly
返回 NO
。代碼以下:
@interface BPPerson : NSObject {
NSString *hobby;
NSString *_hobby;
NSString *ishobby;
NSString *_ishobby;
}
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
- (void)setHobby:(NSString *)hobby {
NSLog(@"setHobby == %s", __func__);
}
- (void)_setHobby:(NSString *)hobby {
NSLog(@"setHobby == %s", __func__);
}
複製代碼
一、此時的調用結果以下:
能夠發現確實先調用了 setHobby:
方法,而且四個成員變量均爲nil,由於setHobby:
方法沒有進行賦值操做。
二、接下來註釋該方法,只保留 _setHobby:
調用結果以下:
能夠發現這次直接調用 _setHobby:
方法,四個成員變量依然均爲 nil,代表並未賦值。
三、繼續註釋 _setHobby:
方法,而且看下 accessInstanceVariablesDirectly
分別爲 NO
和 YES
的狀況:
accessInstanceVariablesDirectly
爲NO
時結果以下:此時直接拋出異常 setValue:forUndefinedKey:
,四個成員變量固然不可能賦值。
accessInstanceVariablesDirectly
爲YES
時結果以下:此時發現成員變量 _hobby 成功賦值,其它依然爲nil,也正符合第二條規則的順序的第一個尋找的變量。
經過打印結果能夠發現,對於成員變量的查找順序確實如文檔所說的 _<Key>、_is<Key>、<Key>、is<Key>
。在測試過程當中,曾將成員變量寫做 ishobby、_ishobby
,這樣寫最終致使了異常拋出,也從側面驗證了第三條,找不到時會拋出異常。
四、變量的查找會考慮is<Key> 和 _is<Key>
的變量,那麼對於set方法會不會也有相似的調用呢?在這裏咱們能夠補充下 setIs<Key> 和 _setIs<Key>
的調用狀況驗證。
先增長兩個方法以下:
- (**void**)setIsHobby:(NSString *)hobby {
NSLog(@"setIsHobby == %s", **__func__** );
}
- (**void**)_setIsHobby:(NSString *)hobby {
NSLog(@"_setIsHobby == %s", **__func__** );
}
複製代碼
調用 setValue:forKey:
後,的執行結果以下:
而後註釋 setIsHobby:
方法,再次執行setValue:forKey:
,結果以下:
經過結果能夠發現,實際上在_setHobby:
註釋後,並無立刻查找成員變量,而是繼續查找並調用了setIsHobby:
,可是當setIsHobby:
註釋後,並不會再查找 _setIsHobby:
,而是直接給成員變量賦值。也就是說文檔中所說的第一步中會多查找一個方法setIs<Key>
,可是不會對_setIs<Key>
進行調用。
KVC調用流程整理以下圖:
對於取值過程 valueForKey 的描述以下:
這段描述也能夠分爲幾個流程:
get<Key>、<key>、is<Key>、_<key>
的順序查找方法實現,若是找到就調用,並執行第5步;accessInstanceVariablesDirectly
是否爲 YES
,若是爲YES
,則依次查找成員變量_<key>
, _is<Key>
, <key>
, is<Key>
,若是查找到則進行第5步,不然進行第6步以上流程中有針對數組和集合的處理,而對於字典的處理並不在這一步驟中,這是由於如前文所述,字典的KVC其實是調用本身的 setObject:forKey:
和 objectForKey:
方法,並不會查找自身的成員變量和屬性。
下面對普通對象的 valueForKey:
進行驗證以下:
按照步驟要求給成員變量賦值不一樣的值,同時在實現上述第一步的方法時,再增長 getIsHobby
方法。下面開始進行驗證:
一、直接調用 valueForKey:
,結果以下:
從結果能夠看到,這裏直接調用 getHobby
方法,而且沒有從成員變量取值。
二、下面依次註釋 getHobby
、hobby
、isHobby
、_hobby
,結果依次以下:
分析結果以下:
getHobby
後,結果顯示直接調用了hobby
方法,未從變量取值hobby
後,結果顯示直接調用了isHobby
方法,未從變量取值isHobby
後,結果顯示直接調用了_hobby
方法,未從變量取值_hobby
後,並未調用getIsHobby
和_isHobby
方法,而是直接從變量_hobby
取值由這個結果發現,對於方法的查找次序確實是如第一步 get<Key>、<key>、is<Key>、_<key>
,而且不會調用其餘的方法
三、接下來驗證變量的值的查找,依次註釋_hobby
, _isHobby
, hobby
, isHobby
,發現結果以下:
根據結果能夠發現變量的調用順序確實如_<key>
, _is<Key>
, <key>
, is<Key>
,而且最終未找到時會拋出異常valueForUndefinedKey:
。
在測試過程當中,還有一個細節,就是當註釋_hobby
的賦值語句,但_hobby
定義並未註釋時,打印結果以下:
能夠發現結果此時並無由於_hobby
值爲nil,就去查找_isHobby
,而是直接取_hobby
的值,這一次序不由於變量值而改變,在setValue:forKey:
時,也是這樣。
valueForKey的流程圖以下:
總結起來,本篇主要介紹了KVC的相關內容,主要有如下幾點:
以上即爲KVC的API探索,以及其調用流程,本篇探索就到此爲止,其實還有關於自定義KVC的探索,可是自定義KVC較長,而且須要考慮到數組、字典、集合以及多線程等的狀況,沒有加入本篇章。其實在網絡上有不少大神寫的很厲害,這裏放上其中一個 DIS_KVC_KVO 供你們參考。