導語:
KVC(Key-value coding)鍵值編碼,允許開發(fā)者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值,不需要調(diào)用明確的存取方法。通過這個技術(shù)就可以在運行時動態(tài)地訪問和修改對象的屬性,而不是在編譯時確定,這也是iOS開發(fā)中的黑魔法之一。很多iOS開發(fā)技巧都是基于KVC實現(xiàn)的。
Demo源碼見Github上的工程KVCDemo,主要從以下幾個方面來展開KVC:
- KVC定義
- KVC尋找key策略
- KVC使用keyPath
- KVC處理異常
- KVC鍵值驗證
- KVC處理容器類屬性
KVC定義
KVC的定義是對NSObject的擴展來實現(xiàn)的,Objective-C中有個顯式的NSKeyValueCoding類別名,在文件NSKeyValueCoding.h中
@interface NSObject(NSKeyValueCoding)
所以對于所有繼承了NSObject的類型,都能使用KVC(一些純Swift類和結(jié)構(gòu)體是不支持KVC的,因為沒有繼承NSObject),以下是KVC最為重要的四個方法:
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
NSKeyValueCoding類別中還有其他的一些比較重要方法,如下
// 默認返回YES,表示如果沒有找到set(get)<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設(shè)置成NO就不這樣搜索。
+ (BOOL)accessInstanceVariablesDirectly;
// 如果Key不存在,且無法搜索到任何和Key有關(guān)的字段或者屬性,則最后才會調(diào)用這個方法,默認是拋出異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 和方法valueForUndefinedKey:一樣,但這個方法是設(shè)值的時候才調(diào)用
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 如果調(diào)用setValue方法時給Value傳nil,則會調(diào)用這個方法。
- (void)setNilValueForKey:(NSString *)key;
// KVC提供屬性值正確性驗證的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設(shè)置新值并返回錯誤原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 集合操作的API,如果屬性是一個NSMutableArray,那么可以用這個方法來返回,這個用于KVC監(jiān)聽屬性為NSMutableArray*類型。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 輸入一組key,返回該組key對應(yīng)的Value,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC尋找key策略
一般開發(fā)過程中,怎么用KVC大部分開發(fā)都比較清楚,就那么幾個常用方法調(diào)用就可以,但是不是很清楚key的尋找策略,通過Demo來驗證下key的尋找順序。
設(shè)值
設(shè)值會調(diào)用方法setValue:forKey:,設(shè)置方法代碼底層的執(zhí)行機制大致步驟如下流程圖:
- 程序優(yōu)先調(diào)用set<Key>:或_set<Key>方法,代碼通過setter方法完成設(shè)置。
注意,這里的<key>是指成員變量名,首字母大小寫要符合KVC的命名規(guī)則,下同 - 如果沒有找到set<Key>:方法,KVC機制會搜索該類里面有沒有名為<key>的成員變量,無論該變量是在類接口處定義,還是在類實現(xiàn)處定義,也無論用了什么樣的訪問修飾符,只在存在以<key>命名的變量,KVC都可以對該成員變量賦值。
- 如果該類即沒有set<Key>:方法,也沒有_<key>成員變量,KVC機制會搜索_is<Key>的成員變量。
- 和上面一樣,如果該類即沒有set<Key>:方法,也沒有_<key>和_is<Key>成員變量,KVC機制再會繼續(xù)搜索<key>和is<Key>的成員變量。再給它們賦值。
- 如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會執(zhí)行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。
簡單說KVC機制在設(shè)值的時候會按照set<Key>: 》_set<Key> 》_<key> 》_is<Key> 》<key> 》 is<Key>順序搜索成員并進行賦值操作,但是如果開發(fā)者重寫了類方法+ (BOOL)accessInstanceVarialbesDirectly并且讓其返回NO,這樣在搜索的時候會直接從步驟1跳轉(zhuǎn)到步驟5。
KVC機制設(shè)值順序效果可以看KVCDemo中對KVCModel的各個name設(shè)值,然后查看下KVCmodel的對象信息:
@interface KVCModel ()
{
NSString* _name;
BaseModel* _name2;
NSString* _isName2;
NSString* _isName3;
NSString* name3;
NSString* name4;
NSString* isName4;
NSString* isName5;
}
@end
@implementation KVCModel
- (void)setName:(NSString*)name
{
NSLog(@"%s name=%@", __FUNCTION__, name);
_name = name;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
NSLog(@"%s value=%@, 該key=%@不存在!", __FUNCTION__, value, key);
}
- (id)valueForUndefinedKey:(NSString *)key
{
NSLog(@"%s,該key不存在%@", __FUNCTION__, key);
return nil;
}
調(diào)用設(shè)值如下:
KVCModel* model = [KVCModel new];
[model setValue:@"newName" forKey:@"name"];
[model setValue:@"newName2" forKey:@"name2"];
[model setValue:@"newName3" forKey:@"name3"];
[model setValue:@"newName4" forKey:@"name4"];
[model setValue:@"newName5" forKey:@"name5"];
[model setValue:@"newName6" forKey:@"name6"];
打斷點查看model的信息,如下: 
同時日志輸出如下:
2018-11-28 19:32:36.482351+0800 KVCDemo[46228:1428290] -[KVCModel setName:] name=newName
2018-11-28 19:32:36.482508+0800 KVCDemo[46228:1428290] -[KVCModel setValue:forUndefinedKey:] value=newName6, 該key=name6不存在!
重寫類方法accessInstanceVariablesDirectly并返會NO,效果可以查看KVCNoAccessInstanceModel的邏輯處理:
對應(yīng)日志輸出如下:
2018-11-28 19:38:03.325658+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel setName:] name=newName
2018-11-28 19:38:03.325795+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel setValue:forUndefinedKey:] value=newKey, 該key=key不存在!
2018-11-28 19:38:03.325924+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel valueForUndefinedKey:],該key不存在name
2018-11-28 19:38:03.326002+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel getKey]
注意:在KVCModel中_name2定義成自定義類BaseModel,但是通過方法setValue:forKey:設(shè)置NSString*,打斷點會發(fā)現(xiàn)_name2類型已經(jīng)變?yōu)镹SString*,本來BaseModel有個方法獲取屬性num值,在設(shè)值后去調(diào)用會出現(xiàn)crash。還有一種情況是如果name類型為基本數(shù)據(jù)類型(BOOL,int等),類型是不會改變。
取值
當調(diào)用valueForKey:方法時,KVC對key的搜索順序有點不同于setValue:forKey:方法,大致步驟如下:
- 按順序查找方法
get<Key>, <key>, is<Key>,如果其中一種方法找到直接調(diào)用,如果是BOOL或者int等值類型,會將包裝成NSNumber對象。 - 如果步驟1的幾個getter方法都沒有找到,KVC機制會查找是否實現(xiàn)了方法
countOf<Key>,同時還實現(xiàn)了兩個方法(objectIn<Key>AtIndex和<Key>AtIndexes)中的一個即可。如果都實現(xiàn)就會返回一個可以響應(yīng)NSArray所有方法的代理集合(它是 NSKeyValueArray,是NSArray的子類),調(diào)用這個代理集合的方法,或者說給這個代理集合發(fā)送屬于NSArray的方法,就會以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes這幾個方法組合的形式調(diào)用,在這幾個函數(shù)打斷點查看會發(fā)現(xiàn)還有一個可選方法get<Key>:range:。所以想重新定義KVC的一些功能,可以添加這些方法,添加的時候注意方法名要符合KVC標準命名方法。 - 步驟2沒有找到,就會同時查找
countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果這三個方法都找到,就會返回一個可以響應(yīng)NSSet所有方法的代理集合,同步驟2一樣的形式調(diào)用。 - 步驟3也沒找到就會檢查類方法+ (BOOL)accessInstanceVarialbesDirectly,如果返回是YES的,和設(shè)值一樣的順序,按
_<key> 》_is<Key> 》<key> 》is<Key>搜索成員變量名,當然這種方法不推薦這么做,這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。這樣還沒找到就會調(diào)用valueForUndefinedKey:方法,默認拋出異常。
步驟2的效果可以看KVCModel中實現(xiàn)的方法:
#pragma mark - 當key使用name6時,KVC會找到這兩個方法。
- (NSUInteger)countOfName7
{
NSLog(@"%s enter", __FUNCTION__);
return 4;
}
-(id)objectInName7AtIndex:(NSUInteger)index
{
NSLog(@"%s enter", __FUNCTION__);
return @(index * 2);
}
其他情況搜索邏輯可以見KVCDemo對KVCModel數(shù)據(jù)獲取,輸出日志如下:
2018-11-28 20:09:28.786934+0800 KVCDemo[51488:1543594] key=name value=newName
2018-11-28 20:09:28.787015+0800 KVCDemo[51488:1543594] -[KVCModel name2] enter
2018-11-28 20:09:28.787074+0800 KVCDemo[51488:1543594] key=name2 value=(null)
2018-11-28 20:09:28.787175+0800 KVCDemo[51488:1543594] key=name3 value=newName3
2018-11-28 20:09:28.787301+0800 KVCDemo[51488:1543594] key=name4 value=newName4
2018-11-28 20:09:28.787419+0800 KVCDemo[51488:1543594] key=name5 value=newName5
2018-11-28 20:09:28.787519+0800 KVCDemo[51488:1543594] -[KVCModel valueForUndefinedKey:],該key不存在name6
2018-11-28 20:09:28.787580+0800 KVCDemo[51488:1543594] key=name6 value=(null)
2018-11-28 20:09:28.787719+0800 KVCDemo[51488:1543594] -[KVCModel countOfName7] enter
2018-11-28 20:09:28.787778+0800 KVCDemo[51488:1543594] -[KVCModel countOfName7] enter
2018-11-28 20:09:28.787836+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
2018-11-28 20:09:28.787882+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
2018-11-28 20:09:28.787959+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
2018-11-28 20:09:28.788027+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
2018-11-28 20:09:28.788120+0800 KVCDemo[51488:1543594] key=name7 value=(
0,
2,
4,
6
)
KVC使用keyPath
類的成員變量有可能是自定義類或其他復雜數(shù)據(jù)類型,對這種成員變量可以先用KVC獲取該屬性,然后再用KVC來獲取這個自定義類的屬性,這樣一層層去獲取,但這樣比較繁瑣。對此KVC提供一個解決方案,就是鍵路徑keyPath,顧名思義就是按照路徑尋找key。主要有兩個以下兩個方法:
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
代碼實現(xiàn)見對KVCModel對象邏輯處理:
NSLog(@"keyPath=baseModel.num value=%@", [model valueForKeyPath:@"baseModel.num"]);
[model setValue:@"newNumValue" forKeyPath:@"baseModel.num"];
NSLog(@"keyPath=baseModel.num value=%@", [model valueForKeyPath:@"baseModel.num"]);
KVC對于keyPath的搜索機制第一步就是分離key,用小數(shù)點.來分割key,然后再像普通key一樣按照上面介紹的順序搜索。
KVC處理異常
使用KVC過程中最常見的異常就是不小心使用了錯誤的key,或者在設(shè)值中不小心傳了nil的值,KVC有專門的方法處理這些異常。
-
KVC處理nil異常,如果在設(shè)值過程中,不小心傳了nil,KVC會調(diào)用方法setNilValueForKey:,這個默認方法是拋出異常,所以一般而言最好重寫這個方法。 -
KVC處理UndefinedKey異常,如果在設(shè)值取值傳的key不存在,這樣的操作就會crash,設(shè)值會調(diào)用到setValue:forUndefinedKey:方法,而取值會調(diào)用valueForUndefinedKey:方法,這兩個方法默認都是拋出異常,所以最好重寫這兩個方法來規(guī)避crash。
KVC鍵值驗證
KVC提供了驗證key對應(yīng)的value是否可用的方法:
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
方法實現(xiàn)會取探索類是否實現(xiàn)了方法
-(BOOL)validateName8:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
如果有實現(xiàn)這個方法,就用實現(xiàn)的方法返回,沒有就直接返回YES,這是默認返回結(jié)果,效果可以看KVCDemo中對屬性name8的驗證。
注意:鍵值驗證不會主動去做驗證,需要開發(fā)者手動去驗證,所以即使類實現(xiàn)了驗證方法,但是KVC也不會主動驗證,還是會設(shè)值成功。當然也有一些技術(shù),比如CoreData會自動調(diào)用。
KVC處理容器類屬性
對象的屬性可以是一對一的,也可以是一對多的。一對多的屬性要么是有序的數(shù)組,要么就是無序的集合。
不可變的有序數(shù)組屬性(NSArray)和無序集合(NSSet)屬性可使用方法valueForKey:來獲取。而當對象的屬性是可變的容器時,對于有序的容器,可以用以下方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
該方法返回一個可變有序數(shù)組。對于無序的容器,可以用以下方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
該方法返回一個可變的無序集合。同時他們也有對應(yīng)的keyPath版本:
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
可變的容器獲取方法,一般在KVO對容器監(jiān)聽的時候會用到,具體使用可見《iOS KVO原理探究》
當NSDictionary對象使用KVC時,valueForKey:的表現(xiàn)行為和objectForKey:一樣,使用valueForKeyPath:可訪問多層嵌套的字典會方便點,在KVC中有兩個關(guān)于NSDictionary的方法:
// 輸入一組key,返回這組key對應(yīng)的屬性,再組成一個字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
// 用來修改Model中對應(yīng)key的屬性
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
結(jié)尾
KVC作為在iOS開發(fā)中的利器,基于運行時的編程方式極大提高了靈活性,簡化了代碼,可以實現(xiàn)一些難以想象的功能,是許多iOS開發(fā)黑魔法的基礎(chǔ),一般有以下幾個場景:
動態(tài)的取值和設(shè)值訪問和修改私有變量修改一些控件的內(nèi)部屬性操作容器類(NSArray和NSSet等)-
實現(xiàn)高階消息傳遞
當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每一個對象,而不是容器本身進行操作,結(jié)果會添加進返回的容器中,這樣開發(fā)就很方便操作集合來返回另一個集合,如以下代碼:
NSArray* languageArrStr = @[@"english", @"franch", @"chinese"];
NSArray* languageArrCapStr = [languageArrStr valueForKey:@"capitalizedString"];
for (NSString* languageStr in languageArrCapStr)
{
NSLog(@"%@", languageStr);
}
打印結(jié)果:
2018-11-29 14:13:07.940471+0800 KVCDemo[17618:2982517] English
2018-11-29 14:13:07.940554+0800 KVCDemo[17618:2982517] Franch
2018-11-29 14:13:07.940694+0800 KVCDemo[17618:2982517] Chinese
方法 capitalizedString被傳遞到NSArray的每一項,這樣NSArray的每一員都會執(zhí)行capitalizedString并返回一個包含結(jié)果的 新NSArray,可從打印結(jié)果看出成功的把首字符都轉(zhuǎn)成大寫。