KVC
KVC(Key-Value-Coding)是Cocoa框架為我們提供的非常強大的工具,簡譯為鍵值編碼。iOS的開發(fā)中,可以允許開發(fā)者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不需要調用明確的存取方法。這樣就可以在運行時動態(tài)地訪問和修改對象的屬性,而不是在編譯時確定,這也是iOS開發(fā)中的黑魔法之一。KVC依賴于RunTime,在Objective-C的動態(tài)性方面發(fā)揮了重要作用。很多高級的iOS開發(fā)技巧都是基于KVC實現(xiàn)的。
KVC的主要功能是直接通過變量名稱字符串來訪問成員變量,不管是私有的還是公有的,這也就是為什么對于Objective-C來說,沒有真正的私有變量。因為一是可以利用RunTime直接獲取所有成員變量,二是通過KVC對成員變量進行訪問讀寫。
基本內容
無論是Swift還是Objective-C,KVC的定義都是對NSObject的擴展來實現(xiàn)的(Objective-C中有個顯式的NSKeyValueCoding類別名,而Swift沒有,也不需要)。所以對于所有直接或者間接繼承了NSObject的類型,也就是幾乎所有的Objective-C對象都能使用KVC(一些純Swift類和結構體是不支持KVC的),下面是KVC重要的四個方法
- (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來設值
KVC中key的查找順序
設置值的查找順序
1、程序優(yōu)先調用
set<Key>:屬性值方法,代碼通過setter方法完成設置。注意,這里的<key>是指成員變量名2、如果沒有找到
set<Key>:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那么在這一步KVC會執(zhí)行setValue:forUndefinedKey:方法。在方法返回YES的情況下,緊接著就會查找_<key>,如果沒有_<key>成員變量,KVC機制會搜索_is<Key>的成員變量。3、如果該類既沒有
set<Key>:方法,也沒有_<key>和_is<Key>成員變量,KVC機制再會繼續(xù)搜索<key>和is<Key>的成員變量。再給它們賦值。4、如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會執(zhí)行該對象的
setValue:forUndefinedKey:方法,默認是拋出異常,如果實現(xiàn)了該方法,那么不會拋出異常。
具體例子分析
創(chuàng)建一個Person類,實現(xiàn)如下
@interface Person : NSObject
@end
@implementation Person
{
NSString *setName;
NSString *isName;
NSString *_name;
NSString *_isName;
NSString *name;
}
- (void)setName:(NSString *)name {
setName = name;
NSLog(@"setName: %@", name);
}
- (NSString *)getName {
NSLog(@"getName");
return setName;
}
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"setValueForUndefinedKey: %@", key);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"valueForUndefinedKey: %@", key);
return nil;
}
@end
測試部分
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];
[person setValue:@"newName" forKey:@"name"];
NSString *name = [person valueForKey:@"name"];
NSLog(@"name: %@", name);
}
1、將accessInstanceVariablesDirectly注釋掉,運行程序,將執(zhí)行setName和getName
setName: newName
getName
name: newName
也就是說先調用set<Key>:方法
2、接下來將setName方法注釋掉,并且修改getName方法,將返回值修改為_name
//- (void)setName:(NSString *)name {
// setName = name;
// NSLog(@"setName: %@", name);
//}
- (NSString *)getName {
NSLog(@"getName:%@", _name);
return _name;
}
運行程序,結果如下
getName:newName
name: newName
也就是滿足了第二步,如果沒有找到set<Key>:方法,就會查找_<key>為其賦值
3、注釋掉_name,并修改getName方法,返回_isName
@implementation Person
{
NSString *setName;
NSString *isName;
// NSString *_name;
NSString *_isName;
NSString *name;
}
- (NSString *)getName {
NSLog(@"getName:%@", _isName);
return _isName;
}
運行程序,結果如下
getName:newName
name: newName
后面2步就不再驗證,有興趣可以自己嘗試。
接下來驗證accessInstanceVariablesDirectly屬性,代碼回復到最初狀態(tài),并且將setName和getName注釋
@implementation Person
{
NSString *setName;
NSString *isName;
NSString *_name;
NSString *_isName;
NSString *name;
}
//- (void)setName:(NSString *)name {
// setName = name;
// NSLog(@"setName: %@", name);
//}
//
//- (NSString *)getName {
// NSLog(@"getName");
// return setName;
//}
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"setValueForUndefinedKey: %@", key);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"valueForUndefinedKey: %@", key);
return nil;
}
@end
依然運行程序,結果如下
setValueForUndefinedKey: name
valueForUndefinedKey: name
name: (null)
說明再找不到setName:方法后,不再去找name系列成員變量,而是直接調用setValue:forUndefinedKey:方法
獲取值的查找順序
1、首先按
get<Key>,<key>,is<Key>的順序查找getter方法,找到直接調用。如果是BOOL、int等內建值類型,會做NSNumber的轉換。2、如果上面的
getter沒有找到,KVC則會查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外兩個方法中的一個被找到,那么就會返回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發(fā)送屬于NSArray的方法,就會以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes這幾個方法組合的形式調用。還有一個可選的get<Key>:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。3、還沒找到,查找
countOf<Key>、enumeratorOf<Key>、memberOf<Key>格式的方法。如果這三個方法都找到,那么就返回一個可以響應NSSet所有方法的代理集合。4、還是沒找到,如果類方法
accessInstanceVariablesDirectly返回YES。那么按_<key>,_is<Key>,<key>,is<key>的順序搜索成員名。5、再沒找到,調用
valueForUndefinedKey。
在KVC中使用keyPath
開發(fā)過程中,一個類的成員變量有可能是自定義類或其他的復雜數(shù)據(jù)類型,你可以先用KVC獲取該屬性,然后再次用KVC來獲取這個自定義類的屬性,但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通過KeyPath來設值
具體例子
@interface Account : NSObject
@property (nonatomic, copy) NSString *password;
@end
@implementation Account
@end
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *age;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, strong) Account *count;
@end
Example
Person *person = [[Person alloc]init];
Account *account = [[Account alloc] init];
account.password = @"xxxx";
person.account = account;
NSString *password1 = [person valueForKeyPath:@"account.password"];
[person setValue:@"yyyy" forKeyPath:@"account.password"];
NSString *password2 = [person valueForKeyPath:@"account.password"];
NSLog(@"password1 = %@, password2 = %@", password1, password2);
// password1 = xxxx, password2 = yyyy
對象關系映射
ORM(Object Relational Mapping,對象關系映射),說白了就是將JSON轉換為對象。在iOS開發(fā)最初的一段時間,還沒有特別好的第三方Model解析庫,那時候基本上是使用NSKeyValueCoding提供的方法
Example
// Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *age;
@property (nonatomic, copy) NSString *address;
@end
NS_ASSUME_NONNULL_END
// Person.m
#import "Person.h"
@implementation Person
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"UndefinedKey: %@", key);
}
@end
// ViewController.m
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSDictionary *dic = @{@"name": @"xiao",
@"age": @22,
@"address": @"China",
@"phone": @"186xxxxxxxx"
};
Person *person = [[Person alloc]init];
[person setValuesForKeysWithDictionary:dic];
}
@end
對私有屬性的訪問
在我們使用一些系統(tǒng)控件時,對一些內部屬性系統(tǒng)往往并沒有暴露給我們,需要我們使用KVC進行訪問。
例如,在使用UITextField時,對于設置placeholder的textColor時只能通過attributedPlaceholder這樣的NSAttributedString來設置,并且每次更改都需要這樣設置一遍,有些麻煩。這里可以通過KVC來獲取_placeholderLabel并對其賦值,修改顏色
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(50, 100, 300, 44)];
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.placeholder = @"please input something....";
UILabel *placeLabel = [textField valueForKey:@"_placeholderLabel"];
placeLabel.textColor = [UIColor redColor];
[self.view addSubview:textField];
效果如下圖

但是需要注意:
蘋果對一些系統(tǒng)控件的實現(xiàn)過程中,很多子控件使用了懶加載,即用到時才會去創(chuàng)建實例,所以使用KVC進行訪問時,需要注意訪問的時機。
例如上面,應該在placeholder設置值之后才訪問,否則_placeholderLabel獲取為nil,textColor設置無效。
另外一個需要注意的地方:雖然采用KVC訪問一些私有成員變量不屬于使用私有API,上線時不太會因此被拒絕,但是私有的成員變量可能會隨著iOS版本的不同而有所變化。
所以使用KVC訪問私有變量時需要謹慎。
控制是否觸發(fā)setter、getter方法
有些時候為了監(jiān)控某個屬性的值訪問情況會重寫setter或getter方法,但只在特定的情況下觸發(fā),通過其他方式不觸發(fā)setter或getter,我們可以通過KVC來做。例如:上面的Person類,重寫name屬性的setter方法,如下:
// Person.m
- (void)setName:(NSString *)name {
_name = name;
NSLog(@"setName: %@", name);
}
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
NSDictionary *dic = @{@"name": @"xiao",
@"age": @22,
@"address": @"China",
@"phone": @"186xxxxxxxx"
};
Person *person = [[Person alloc]init];
[person setValuesForKeysWithDictionary:dic];
[person setValue:@"zhangsan" forKey:@"_name"];
}
運行輸出
undefinedKey: phone
setName: xiao
只有在 [person setValuesForKeysWithDictionary:dic];時觸發(fā)過一次setName:的方法,而通過KVC給_name賦值并不會觸發(fā),如果也想觸發(fā),可以將“_name”改成“name”來實現(xiàn)。
[person setValue:@"zhangsan" forKey:@"name"];
這樣運行輸出
undefinedKey: phone
setName: xiao
setName: zhangsan
可以根據(jù)實際需求,選擇使用KVC的方法,出現(xiàn)上面的情況是因為KVC的查找成員變量的機制。
查找成員變量的機制
如果一個實例對象用KVC來訪問其成員變量,則會按照以下的順序來進行查找,例如:我們調用的方法是:
[person setValue:@"aa" forKey:@"name"];
1、訪問setName方法
2、訪問_name成員變量
3、訪問_isName成員變量
4、訪問name成員變量
5、訪問isName成員變量
以上就是KVC查找的過程,只有在某一步找到才會不繼續(xù)向下查找,否則會按照上面的順序逐個查找,如果到最后一個也找不到,那就會調用)setValue:forUndefinedKey:方法。
值得注意的是,KVC的協(xié)議NSKeyValueCoding中的accessInstanceVariablesDirectly屬性:
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
該屬性默認為YES,如果重寫返回NO,則下面這些方法都將不起作用
-valueForKey:, -setValue:forKey:, -mutableArrayValueForKey:,
-storedValueForKey:, -takeStoredValue:forKey:, and -takeValue:forKey:
也就是相當于禁止KVC的方法。但是我們在之前使用setValuesForKeysWithDictionary:方法仍然可以使用。
KVC進階用法
KVC的使用可不僅僅是訪問成員變量這么簡單,蘋果為KVC提供了一些高級的用法,方便開發(fā)者在代碼中的使用
1、keyPath訪問
對于keyPath訪問很多人應該不陌生,在一些ORM庫中經常會指向通過keyPath來映射賦值。
舉個例子:回到剛才的textFiled的場景,我們知道有一個“_placeholderLabel”成員變量,它是一個UILabel的實例,我們獲取到該UILabel,并對其進行textColor屬性賦值,達到了想要的效果。但實際上可以一步完成上面的操作
UITextField *textField = [[UITextField alloc]initWithFrame:CGRectMake(100, 100, 200, 50)];
textField.placeholder = @"placeholder";
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[self.view addSubview:textField];
2、集合類型的訪問
集合類型包括數(shù)組、字典和集合,其中,集合還分為有序和無序。對于我們的Person類,現(xiàn)在需要一個屬性friends:
@property(nonatomic, strong) NSMutableArray *friends;
然后在Person.m文件中會有提示下面等一系列方法
-(id)objectInFriendsAtIndex:(NSUInteger)index
-(NSArray *)friendsAtIndexes:(NSIndexSet *)indexes
-(NSUInteger)countOfFriends
這是KVC幫我們針對friends屬性添加的一系列方法中的幾個,表示對friends屬性支持KVC集合類型,目的是方便我們使用KVC時對其進行便捷式訪問。那么什么是便捷式訪問呢?
假設一下,如果我們要通過KVC獲取到Person對象的friends屬性,并添加一個friend,如果不使用KVC集合類型訪問,可能需要這樣寫:
//方式1
NSMutableArray *friends = [person valueForKey:@"friends"];
[friends addObject:[Person new]];
// 當然也可以使用該方法
[person.friends addObject:[Person new]];
通過KVC方式,我們可以簡化方式1的操作
[[person mutableArrayValueForKey:@"friends"] addObject:[Person new]];
除了簡化代碼這一點,實際上這種KVC集合類型還有一個好處,就是可以對于不可變的集合類型提供安全的可變訪問。
上面部分,我們的friends屬性是可變數(shù)組,如果改成不可變數(shù)組NSArray,那么再對其進行添加對象,使用上面的兩種方法就會不一樣了,前者會直接crash,而后者則會安全訪問,即使是不可變數(shù)組也可以增加數(shù)組元素。
但我們知道對于NSArray是不可變的,這在創(chuàng)建Person實例的時候就已經確定好了,對其添加元素,并不是不可變數(shù)組可以添加元素,而是在用KVC進行集合類型訪問時,如果是不可變數(shù)組,在添加元素時會重新創(chuàng)建一個不可變數(shù)組對象,然后將friends屬性指向新創(chuàng)建的不可變數(shù)組。
雖然使用這種方式不會引起奔潰,但是在創(chuàng)建Person類時,既然將friends屬性設置為不可變數(shù)組,那么就應該避免再向其添加對象,因為這與最初的邏輯相左。
3、KVC驗證
使用KVC也是有風險的,因為通過字符串去訪問實例變量,雖然KVC提供類復雜的查找邏輯來幫助找到對應的成員變量,但是仍然會發(fā)生找不到的情況。
例如:我們使用-setValue:forKey來對對象進行賦值訪問,當-setValue:forUndefinedKey沒有實現(xiàn),而且如果key不存在,將會導致奔潰。
[person setValue:@"123" forKey:@"key"];
如果想避免奔潰,實現(xiàn)-setValue:forUndefinedKey,打印不存在的key,或者使用try-catch,如下使用try-catch捕獲異常,得到相關信息
NSDictionary *dic = @{@"name": @"xiao",
@"age": @22,
@"address": @"China"
};
Person *person = [[Person alloc]init];
[person setValuesForKeysWithDictionary:dic];
@try {
[person setValue:@"123" forKey:@"thought"];
} @catch (NSException *exception) {
NSLog(@" %@", exception.userInfo);
} @finally {
}
打印如下:
{
NSTargetObjectUserInfoKey = "<Person: 0x600000ed3840>";
NSUnknownUserInfoKey = thought;
}
NSUnknownUserInfoKey所對應的值就是不存在的key
除了上面的方法,KVC還提供了一種值驗證的方法
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue
forKey:(NSString *)inKey
error:(out NSError * _Nullable __autoreleasing *)outError
該方法是驗證值是否符合key所對應的類型,或者說值類型是否正確。方法中的ioValue參數(shù)是要賦值二級指針類型。如果不是我們想要的值,則可以直接改變ioValue的指向,也就是重新指向一個正確的值。
例如:我們現(xiàn)在驗證Person實例的字符串屬性name,因此調用驗證方法來判斷
UIColor *color = [UIColor yellowColor];
NSError *error = nil;
BOOL isOK = [person validateValue:&color forKeyPath:@"name" error:&error];
name屬性需要接受字符串類型的值,很顯然我們傳一個color是錯誤的,然而通過運行發(fā)現(xiàn)驗證的返回值是YES,表示驗證通過,這明顯不合理。
根據(jù)官方文檔描述,對于屬性的驗證分為是否必需,默認為不必需,如果是不必需,則會直接返回YES,不會對其進行驗證,而如果需要驗證,則需要在Person.m中實現(xiàn)如下方法
-(BOOL)validateName:(id *)ioValue error:(NSError **)error {
if ([*ioValue isKindOfClass:NSString.class]) {
return YES;
}
return NO;
}
現(xiàn)在再去驗證,會發(fā)現(xiàn)返回NO,符合我們的預期。
4、函數(shù)操作
同樣還是對于一些集合類型的數(shù)據(jù),我們希望可以利用共同性去做一些快捷的操作,例如求平均值和求和等,不需要再去for循環(huán)或者枚舉。
例如:有一個Person類型的數(shù)組,想求所有person的age之和
NSDictionary *dic1 = @{@"name": @"1",
@"age": @22
};
NSDictionary *dic2 = @{@"name": @"2",
@"age": @21
};
NSDictionary *dic3 = @{@"name": @"3",
@"age": @23
};
Person *person1 = [[Person alloc]init];
Person *person2 = [[Person alloc]init];
Person *person3 = [[Person alloc]init];
[person1 setValuesForKeysWithDictionary:dic1];
[person2 setValuesForKeysWithDictionary:dic2];
[person3 setValuesForKeysWithDictionary:dic3];
NSArray *persons = @[person1, person2, person3];
NSNumber *sumAge = [persons valueForKeyPath:@"@sum.age"];
NSLog(@"sumAge: %@", sumAge); // 66
上面獲取的就是所有人的age之和,不用for循環(huán),直接使用KVC實現(xiàn)。
注意:使用的是KeyPath,并且sum前面加一個@表示是數(shù)組特有的鍵.
除此之外,還可以求出數(shù)組的平均值、最大值、最小值
NSNumber *count = [persons valueForKeyPath:@"@count"];
NSLog(@"count: %@", count); // 3
NSNumber *ageAve = [persons valueForKeyPath:@"@avg.age"];
NSLog(@"avg: %@", ageAve); // 22
NSNumber *maxAge = [persons valueForKeyPath:@"@max.age"];
NSLog(@"max: %@", maxAge); // 23
NSNumber *minAge = [persons valueForKeyPath:@"@min.age"];
NSLog(@"min: %@", minAge); // 21
在NSKeyValueCoding.h文件中,定義了一系列的NSKeyValueOperator,而這些Operator都是為數(shù)組類型準備的。
更多詳細內容可以參考這里