一.什么是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;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
当然NSKeyValueCoding类别中还有其他的一些方法,下面列举一些:
+ (BOOL)accessInstanceVariablesDirectly;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
上面的这些方法在碰到特殊情况或者有特殊需求还是会用到的,所以也是可以稍微了解一下。后面的代码示例会有讲到其中的一些方法。
三.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 *_isName;
}
@end
NS_ASSUME_NONNULL_END
#import "Dog.h"
@implementation Dog
+ (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 {
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];
NSLog(@"%lu",(unsigned long)[ar count]);
[arr incrementCount];
NSLog(@"%lu",(unsigned long)[ar count]);
[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;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)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: 就没问题了:
@implementation People
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能将%@设成nil",key);
}
@end
打印结果:
|