準備工作
KVC協議定義
KVC
是NSKeyValueCoding
的簡寫,鍵值編碼是由NSKeyValueCoding
非正式協議啟用的一種機制,對象采用該機制來提供對其屬性
的間接訪問。當對象符合鍵值編碼時,其屬性可通過字符串參數通過簡潔、統一的消息傳遞接口
進行尋址
。這種間接訪問機制補充了實例變量及其相關訪問器方法提供的直接訪問。
KVC在Objective-C中的定義
KVC
的定義都是對NSObject的擴展
來實現的,查看setValueForKey
方法,發現其在Foundation
里面,而Foundation
框架是不開源
的,只能在蘋果官方文檔查找。見下圖:
KVC提供的API方法
- 我們可以通過官方提供的文檔進行查看(文章開頭有鏈接)
- 蘋果對一些容器類比如
NSArray
或者NSSe
t等,KVC
有著特殊的實現。
常用方法
對于所有繼承了NSObject
的類型,也就是幾乎所有的Objective-C
對象都能使用KVC
,下面是KVC
最為重要的四個方法:
- (nullable id)valueForKey:(NSString *)key; // 直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; // 通過Key來設值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通過KeyPath來設值
特殊方法
NSKeyValueCoding
類別中還有其他的方法,當我們遇到適合的需求時,就能夠派上用場了。方法如下:
// 默認返回YES,表示如果沒有找到Set方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly;
// KVC提供屬性值正確性驗證的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設置新值并返回錯誤原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個NSMutableArray,那么可以用這個方法來返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果Key不存在,且沒有KVC無法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 和上一個方法一樣,但這個方法是設值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 如果你在SetValue方法時面給Value傳nil,則會調用這個方法
- (void)setNilValueForKey:(NSString *)key;
// 輸入一組key,返回該組key對應的Value,再轉成字典返回,用于將Model轉到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC常用案例
-
結構體的處理
KVC
在進行結構體處理時,需要用到NSValue
,設值時,將結構體封裝成NSValue
,進行鍵值設值
;取值同樣返回NSValue
,然后按照結構體格式進行解析
,見下面代碼:
// 結構體
ThreeFloats floats = {1.,2.,3.};
// 封裝成NSValue
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
// 設值
[person setValue:value forKey:@"threeFloats"];
// 取值
NSValue *value1 = [person valueForKey:@"threeFloats"];
// 結構體解析
ThreeFloats th;
[value1 getValue:&th];
NSLog(@"%f-%f-%f",th.x,th.y,th.z);
-
字典處理(模型轉換)
字典可以實現與模型進行裝換,也可以通過鍵值數組從模型中獲取字典數據。實現代碼如下:
- (void)dictionaryTest{
// 字典
NSDictionary* dict = @{
@"name":@"Cooci",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@180
};
// 模型
LGStudent *p = [[LGStudent alloc] init];
// 字典轉模型
[p setValuesForKeysWithDictionary:dict];
// 鍵值數組
NSArray *array = @[@"name",@"age"];
// 從模型中獲取響應的字典數據
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);
}
KVC設值取值順序
KVC
的使用相信是沒什么難度的,但是它尋找key
的過程是怎么樣子的呢?以下就進行分析。
設值
當調用setValue:forKey:
代碼時,會有什么的內部操作呢?我在其官方文章中找到下圖:
上圖的意思是:
setValue:forKey:
的默認實現,給定key
和value
參數作為輸入,嘗試將名為key
的屬性設置為value
,在接收調用的對象內部,使用以下過程:按順序查找名為 set<Key>:
或 _set<Key>
的第一個訪問器。 如果找到,則使用輸入值(或根據需要展開的值)調用它并完成。如果未找到簡單訪問器,并且類方法 accessInstanceVariablesDirectly
返回 YES
,則按順序查找名稱類似于 _<key>
、_is<Key>
、<key>
或 is<Key>
的實例變量。 如果找到,直接使用輸入值(或解包值)設置變量并完成。在未找到訪問器或實例變量時,調用
setValue:forUndefinedKey:
。 默認情況下,這會引發異常,但 NSObject
的子類可能會提供特定于鍵的行為。
根據上面的解析可以總結為以下的幾點:
- 按順序查找名為
set<Key>
,_set<Key>
或者setIs<Key>
的setter
訪問器順序查找,如果找到就調用。只要實現任意一個
,那么就會將調用這個方法,將屬性的值設為傳進來的值
。 - 如果沒有找到這些
setter
方法,KVC
機制會檢查+ (BOOL)accessInstanceVariablesDirectly
方法有沒有返回YES
,默認該方法會返回YES
,如果重寫了該方法讓其返回NO
的話,那么在這一步KVC
會執行setValue:forUndefinedKey:
方法。 - 如果返回
YES
,KVC
機制會優先搜索該類里面有沒有名為_<Key>
的成員變量,無論該變量是在類接口處定義,還是在類實現處定義,也無論用了什么樣的訪問修飾符,只在存在以_<Key>
命名的變量,KVC
都可以對該成員變量賦值。 -
KVC
機制再會繼續搜索_is<Key>
、<key>
和is<key>
的成員變量,再給它們賦值。 - 如果上面列出的方法或者成員變量
都不存在
,系統將會執行該對象的setValue:forUndefinedKey:
方法,默認是拋出異常。
以[person setValue:@"newName" forKey:@"name"];
為例,得出結論:
- 優先通過
setter
方法,進行屬性設置,調用順序是:setName
_setName
setIsName
- 果以上方法均未找到,并且
accessInstanceVariablesDirectly
返回YES
,則通過成員變量
進行設置,順序是:_name
_isName
name
isName
注意:以上可以通過案例進行演示,我就不在這里演示了。
補充說明accessInstanceVariablesDirectly
嘗試重寫+ (BOOL)accessInstanceVariablesDirectly
方法讓其返回NO
,如果KVC
沒有找到set<Key>
、_set<Key>
、setIs<Key>
相關方法時,會直接用setValue:forUndefinedKey:
方法。我們用代碼來測試一下上面的KVC機制:
@interface LGPerson : NSObject
{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
@end
@implementation LGPerson
+(BOOL)accessInstanceVariablesDirectly{
return NO;
}
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"出現異常,該key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出現異常,該key不存在%@",key);
}
// 設置方法全部注釋掉
// -(void)setName:(NSString*)name{
// toSetName = name;
// }
// - (void)_setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
// }
// - (void)setIsName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
// }
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson* person = [LGPerson new];
[person setValue:@"NewName" forKey:@"name"];
NSString* name = [person valueForKey:@"name"];
NSLog(@"value for key : %@",name);
NSLog(@"取值_name:%@",person->_name);
NSLog(@"取值_isName:%@",person->_isName);
NSLog(@"取值name:%@",person->name);
NSLog(@"取值isName:%@",person->isName);
}
return 0;
}
運行結果:
這說明了重寫+(BOOL)accessInstanceVariablesDirectly
方法讓其返回NO
后,KVC
找不到set<Key>
等方法后,不再去找<Key>
系列成員變量,而是直接調用setValue:forUndefinedKey:
,如果我們自身的類不需要KVC
機制的話可以這樣子寫。
KVC設值流程圖
取值
同理,在調用valueForKey:
時候會發生什么呢?根據官方的文檔得出:
根據官方文檔得出
valueForKey:
的機制如下:
- 首先按
get<Key>
,<Key>
,is<Key>
,_<Key>
的順序方法查找getter
方法,找到的話會直接調用,如果是BOOL
或者Int
等值類型, 會將其包裝成一個NSNumber
對象。 - 如果上面的
getter
沒有找到,KVC
則會查找countOf<Key>
,objectIn<Key>AtIndex
或<Key>AtIndexes
格式的方法。如果countOf<Key>
方法和另外兩個方法中的一個被找到,那么就會返回一個可以響應NSArray
所有方法的代理集合(它是NSKeyValueArray
,是NSArray
的子類),調用這個代理集合的方法,或者說給這個代理集合發送屬于NSArray
的方法,就會以countOf<Key>
,objectIn<Key>AtIndex
或At<Key>Indexes
這幾個方法組合的形式調用。還有一個可選的get<Key>:range:
方法。所以你想重新定義KVC
的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC
的標準命名方法,包括方法簽名
。 - 如果上面的方法沒有找到,那么會同時查找
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
格式的方法。如果這三個方法都找到,那么就返回一個可以響應NSSet
所的方法的代理集合,和上面一樣,給這個代理集合發NSSet
的消息,就會以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
組合的形式調用。 - 如果還沒有找到,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES(默認行為),那么和先前的設值一樣,會按_<Key>
,_is<Key>
,<Key>
,is<Key>
的順序搜索成員變量名,這里不推薦這么做,因為這樣直接訪問實例變量破壞了封裝性
,使代碼更脆弱
。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly
返回NO
的話,那么會直接調用valueForUndefinedKey:
。 - 還沒有找到的話,調用
valueForUndefinedKey:
以[person valueForKey:@"name"];
為例
-
getter
方法的調用順序是:getName
name
isName
_name
- 如果以上方法沒有找到,
accessInstanceVariablesDirectly
返回YES
,則直接返回成員變量,獲取順序依然是:_name
_isName
name
isName
注意:以上可以通過案例進行演示,我就不在這里演示了。
KVC取值流程圖
代碼驗證KVC取值(需要的話就拷貝運行即可)
@interface LGPerson : NSObject
{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
@end
@implementation LGPerson
+(BOOL)accessInstanceVariablesDirectly{
return NO;
}
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"出現異常,該key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出現異常,該key不存在%@",key);
}
// 設置方法全部注釋掉
// -(void)setName:(NSString*)name{
// toSetName = name;
// }
// - (void)_setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
// }
// - (void)setIsName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
// }
// 取值方法
//- (NSString *)getName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)name{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)isName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)_name{
// return NSStringFromSelector(_cmd);
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson* person = [LGPerson new];
[person setValue:@"NewName" forKey:@"name"];
NSString* name = [person valueForKey:@"name"];
NSLog(@"value for key : %@",name);
NSLog(@"取值_name:%@",person->_name);
NSLog(@"取值_isName:%@",person->_isName);
NSLog(@"取值name:%@",person->name);
NSLog(@"取值isName:%@",person->isName);
}
return 0;
}
在KVC中使用keyPath
除了對當前對象的屬性
進行賦值外,還可以對其更深層
的對象進行賦值。例如,對當前對象的location
屬性的country
屬性進行賦值。KVC
進行多級訪問時,直接類似于屬性調用一樣用點語法進行訪問即可。
[person setValue:@"" forKeyPath:@"location.country"];
通過keyPath
對數組進行取值時,并且數組中存儲的對象類型都相同
,可以通過valueForKeyPath:
方法指定取出數組中所有對象的某個字段。例如下面例子中,通過valueForKeyPath:
將數組中所有對象的name
屬性值取出,并放入一個數組中返回。
NSArray *names = [array valueForKeyPath:@"name"];
異常處理
當根據KVC
搜索規則,沒有搜索到對應的key
或者keyPath
,則會調用對應的異常方法。異常方法的默認實現,在異常發生時會拋出一個異常,并且應用程序Crash
。見下圖:
重寫以下兩個方法,防止
crash
發生:
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"出現異常,該key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出現異常,該key不存在%@",key);
}
再次運行程序,發現不再崩潰:
為了合理處理
KVC
發出的異常,我們還可以這樣子處理:
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
[self setValue:@"" forKey:@”age”];
} else {
[super setNilValueForKey:key];
}
}
自定義KVC的實現
根據蘋果官方文檔提供的設值、取值規則,我們可以自己進行KVC
的自定義實現。見下面實現代碼:
// KVC 自定義
@implementation NSObject (LGKVC)
// 設置
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
// 1: 判斷什么 key
if (key == nil || key.length == 0) {
return;
}
// 2: setter set<Key>: or _set<Key>,
// key 要大寫
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
// 是否存在方法
if ([self lg_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3: 判斷是否響應 accessInstanceVariablesDirectly 返回YES NO 奔潰
// 3:判斷是否能夠直接賦值實例變量——NO
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4: 間接變量
// 獲取 ivar -> 遍歷 containsObjct -
// 4.1 定義一個收集實例變量的可變數組
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// 拼接成員變量
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
// 是否存在對應的變量
if ([mArray containsObject:_key]) {
// 4.2 獲取相應的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 對相應的 ivar 設置值
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5:如果找不到相關實例
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
// 取值
- (nullable id)lg_valueForKey:(NSString *)key{
// 1:刷選key 判斷非空
if (key == nil || key.length == 0) {
return nil;
}
// 2:找到相關方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
// key 要大寫
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3:判斷是否能夠直接賦值實例變量-YES、NO
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相關實例變量進行賦值
// 4.1 定義一個收集實例變量的可變數組
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// _name -> _isName -> name -> isName
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
// 判斷是否存在對應的成員變量
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
return @"";
}
#pragma mark **- 相關方法**
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
- (id)performSelectorWithMethodName:(NSString *)methodName{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}
- (NSMutableArray *)getIvarListName{
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
@end