一.什么是KVC:
KVC
(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS的开发中,可以允许开发者通过Key
名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。
二.KVC在iOS中的定义:
无论是Swift
还是Objective-C
,KVC
的定义都是对NSObject
的扩展来实现的(Objective-C
中有个显式的NSKeyValueCoding
类别名,而Swift
没有,也不需要)。所以对于所有继承了NSObject
的类型,也就是几乎所有的Objective-C
对象都能使用KVC
(一些纯Swift
类和结构体是不支持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类别中还有其他的一些方法,下面列举一些:
+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
上面的这些方法在碰到特殊情况或者有特殊需求还是会用到的,所以也是可以稍微了解一下。后面的代码示例会有讲到其中的一些方法。
三.KVC是怎么寻找Key的:
1.设值:
当调用setValue:属性值 forKey:@"name"
的代码时,底层的执行机制如下:
- 程序优先调用
set<Key>:
属性值方法,代码通过setter
方法完成设置。注意,这里的<key>
是指成员变量名,首字母大小写要符合KVC
的命名规则,下同 - 如果没有找到
setName:
方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回YES
,默认该方法会返回YES
,如果你重写了该方法让其返回NO
的话,那么在这一步KVC
会执行setValue:forUndefinedKey:
方法,不过一般开发者不会这么做。所以KVC
机制会搜索该类里面有没有名为_<key>
的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以_<key>
命名的变量,KVC
都可以对该成员变量赋值。 - 如果该类即没有
set<key>:
方法,也没有_<key>
成员变量,KVC
机制会搜索_is<Key>
的成员变量。 - 和上面一样,如果该类即没有
set<Key>:
方法,也没有_<key>
和_is<Key>
成员变量,KVC
机制再会继续搜索<key>
和is<Key>
的成员变量。再给它们赋值。 - 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的
setValue:forUndefinedKey:
方法,默认是抛出异常。
如果开发者想让这个类禁用KVC
里,那么重写+ (BOOL)accessInstanceVariablesDirectly
方法让其返回NO
即可,这样的话如果KVC
没有找到set<Key>:
属性名时,会直接用setValue:forUndefinedKey:
方法。
下面我们来让代码来测试一下上面的KVC机制:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Dog : NSObject {
NSString *toSetName;
NSString *isName;
//NSString *name;
NSString *_name;
NSString *_isName;
}
@end
NS_ASSUME_NONNULL_END
#import "Dog.h"
@implementation Dog
// - (void)setName:(NSString*)name {
// toSetName = name;
// }
//- (NSString*)getName {
// return toSetName;
//}
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@",key);
}
@end
#import <Foundation/Foundation.h>
#import "Dog.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [Dog new];
[dog setValue:@"newName" forKey:@"name"];
NSString *name = [dog valueForKey:@"toSetName"];
NSLog(@"%@",name);
}
return 0;
}
首先我们先重写accessInstanceVariablesDirectly
方法让其返回NO
,再运行代码(注意上面注释的部分),Xcode直接打印出:
这说明了重写+(BOOL)accessInstanceVariablesDirectly
方法让其返回NO
后,KVC
找不到setName:
方法后,不再去找name
系列成员变量,而是直接调用setValue:forUndefinedKey:
方法,所以开发者如果不想这样,就可以这么做:
下面那两个setter
和getter
的注释取消掉,再把
NSString *name = [dog valueForKey:@"toSetName"];
//换成
NSString *name = [dog valueForKey:@"name"];
XCode就可以正确地打印出正确的值了:
下面再注释掉accessInstanceVariablesDirectly
方法,就能测试其他的key
查找顺序了,为了节省篇幅,剩下的的KVC
对于key
寻找机制就不在这里展示了,有兴趣的可以自己去写代码去验证。
2.取值:
当调用valueForKey:@"name"
的代码时,KVC
对key
的搜索方式不同于setValue:属性值 forKey:@"name"
,其搜索方式如下:
- 首先按
get<Key>
,<key>
,is<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
或<Key>AtIndexes
这几个方法组合的形式调用。还有一个可选的get<Key>:range:
方法。所以你想重新定义KVC
的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC
的标准命名方法,包括方法签名。 - 如果上面的方法没有找到,那么会同时查找
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet
所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
组合的形式调用。
可能上面的两条查找方案可能不好理解,简单来说就是如果你在自己的类自定义了KVC的实现,并且实现了上面的方法,那么恭喜你,你可以将返回的对象当数组(NSArray)用了,详情见下面的示例代码。
- 如果还没有找到,再检查类方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES
(默认行为),那么和先前的设值一样,会按_<key>
,_is<Key>
,<key>
,is<Key>
的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly
返回NO
的话,那么会直接调用valueForUndefinedKey:
- 还没有找到的话,调用
valueForUndefinedKey:
下面再上代码测试:
@interface TwoTimesArray : NSObject
@property (nonatomic,readwrite,assign) NSUInteger count;
@property (nonatomic,copy) NSString *arrName;
- (void)incrementCount;
- (NSUInteger)countOfNumbers;
- (id)objectInNumbersAtIndex:(NSUInteger)index;
@end
@implementation TwoTimesArray
- (void)incrementCount {
self.count++;
}
- (NSUInteger)countOfNumbers {
return self.count;
}
- (id)objectInNumbersAtIndex:(NSUInteger)index { //当key使用numbers时,KVC会找到这两个方法。
return @(index * 2);
}
- (NSInteger)getNum { //第一个,自己一个一个注释试
return 10;
}
- (NSInteger)num { //第二个
return 11;
}
- (NSInteger)isNum { //第三个
return 12;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TwoTimesArray* arr = [TwoTimesArray new];
NSNumber* num = [arr valueForKey:@"num"];
NSLog(@"%@",num);
id ar = [arr valueForKey:@"numbers"];
NSLog(@"%@",NSStringFromClass([ar class]));
NSLog(@"0:%@ 1:%@ 2:%@ 3:%@",ar[0],ar[1],ar[2],ar[3]);
[arr incrementCount]; //count加1
NSLog(@"%lu",(unsigned long)[ar count]); //打印出1
[arr incrementCount]; //count再加1
NSLog(@"%lu",(unsigned long)[ar count]); //打印出2
[arr setValue:@"newName" forKey:@"arrName"];
NSString* name = [arr valueForKey:@"arrName"];
NSLog(@"%@",name);
}
return 0;
}
从打印结果可以看出,它直接调用-(id)objectInNumbersAtIndex:(NSUInteger)index;
方法,充分说明了KVC
在调用ValueforKey:@"name"
时搜索key
的机制。
四.在KVC中使用keyPath:
然而在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath
。
下面是keyPath
的常用方法:
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
下面是简单的代码演示:
@interface Address : NSObject
@end
@interface Address()
@property (nonatomic,copy)NSString* country;
@end
@implementation Address
@end
@interface People : NSObject
@end
@interface People()
@property (nonatomic,copy) NSString* name;
@property (nonatomic,strong) Address* address;
@property (nonatomic,assign) NSInteger age;
@end
@implementation People
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
People* people1 = [People new];
Address* add = [Address new];
add.country = @"China";
people1.address = add;
NSString* country1 = people1.address.country;
NSString * country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@",country1,country2);
[people1 setValue:@"USA" forKeyPath:@"address.country"];
country1 = people1.address.country;
country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@",country1,country2);
}
return 0;
}
打印结果:
上面的代码简单在展示了keyPath
是怎么用的。如果你不小心错误的使用了key
而非keyPath
的话,比如上面的代码中KVC
会直接查找address.country
这个属性,很明显,这个属性并不存在,所以会再调用undefinedKey
相关方法。而KVC
对于keyPath
是搜索机制第一步就是分离key
,用小数点.
来分割key
,然后再像普通key
一样按照先前介绍的顺序搜索下去。
五.KVC如何处理异常:
KVC
中最常见的异常就是不小心使用了错误的key
,或者在设值中不小心传递了nil
的值,KVC
中有专门的方法来处理这些异常。
通常在用KVC
操作Model
时,抛出异常的那两个方法是需要重写的。虽然一般很小出现传递了错误的Key
值这种情况,但是如果不小心出现了,直接抛出异常让APP
崩溃显然是不合理的。一般在这里直接让这个key
打印出来即可,或者有些特殊情况需要特殊处理。通常情况下,KVC
不允许你要在调用setValue:属性值 forKey:@”name“
(或者keyPath
)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil
的。如果你不小心传了,KVC
会调用setNilValueForKey:
方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
[people1 setValue:nil forKey:@"age"];
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<People 0x100200080> setNilValueForKey]: could not set nil as the value for the key age.'
// 调用setNilValueForKey抛出异常
如果重写setNilValueForKey:
就没问题了:
@implementation People
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能将%@设成nil",key);
}
@end
打印结果: