一、KVC简介
KVC(Key-Value Coding)键值编码,是利用
NSKeyValueCoding
非正式协议实现的一种机制,对象采用这种机制来提供对其属性的间接
访问。
当对象采用该协议时,可以通过简洁统一的方法来访问其属性。简单来说,就是我们在开发中可以通过key
名直接访问对象的属性
,或者对属性进行赋值操作,而不需要调用明确的存取方法。这样就允许我们在运行时
去动态
地访问和修改对象的属性,而不是在编译时决定。
二、KVC原理分析
1、KVC设值过程
当我们去调用 setValue:值 forKey:名字
设值方法时,底层的执行机制大致如下:

1、程序会优先去调用
set<Key>
、_set<Key>
、setIs<Key>
方法,如果存在这些命名规则的方法,会直接调用该方法进行赋值。调用优先顺序如上所写
2、如果没有找到 步骤1 的方法,程序会去判断+ (BOOL)accessInstanceVariablesDirectly
方法的返回值,如果该方法返回值为NO
(默认返回YES
,在我们重写该方法时有可能返回NO,一般不会返回NO),则会执行setValue: forUndefinedKey:
方法报错。
3、如果 步骤2 返回YES
,程序会去查找命名方式为
_<key>
、_<isKey>
、<key>
、<isKey>
形式的实例变量,加入存在该形式的实例变量,则会直接将我们调用方法的值赋值给该实例变量。
4、如果 步骤3 没有查找到符合规则的实例变量,程序就会去执行setValue: forUndefinedKey:
方法进行报错。
读万卷书,不如行万里路。下面我们来实例验证下:
@interface CHJManager : NSObject
{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
@implementation CHJManager
+(BOOL)accessInstanceVariablesDirectly {
return YES;
}
- (void)setName:(NSString *)value{
NSLog(@"%s - %@",__func__,value);
}
- (void)_setName:(NSString *)value{
NSLog(@"%s - %@",__func__,value);
}
- (void)setIsName:(NSString *)value{
NSLog(@"%s - %@",__func__,value);
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"糊涂了吧,没有这个!");
}
@end
看到这,你们是不是会觉得为什么我们是用 成员变量
,而不是用属性来做分析呢?
其实是因为成员变量的 单一变量 原则,用属性的话,我们无法来区分。
执行代码:
CHJManager *manager = [[CHJManager alloc] init];
[manager setValue:@"帅帅金" forKey:@"name"];
NSLog(@"%@ - %@ - %@ - %@",manager->_name,manager->_isName,manager->name,manager->isName);
NSLog(@"%@ - %@ - %@",manager->_isName,manager->name,manager->isName);
NSLog(@"%@ - %@",manager->name,manager->isName);
NSLog(@"%@",manager->isName);
打印信息:
2020-03-09 16:09:06.631905+0800 KVC简介[33111:1035025] -[CHJManager setName:] - 帅帅金
2020-03-09 16:09:06.632019+0800 KVC简介[33111:1035025] (null) - (null) - (null) - (null)
2020-03-09 16:09:06.632110+0800 KVC简介[33111:1035025] (null) - (null) - (null)
2020-03-09 16:09:06.632196+0800 KVC简介[33111:1035025] (null) - (null)
2020-03-09 16:09:06.632343+0800 KVC简介[33111:1035025] (null)
由上可知,程序执行了 setName
方法,验证了 步骤1。如果我们注释了 setName
,它会执行 _setName
方法,由此我们可知顺序依次为:setName
、 _setName
、setIsName
。
如果我们将 set
方法都注释掉,然后再将 accessInstanceVariablesDirectly
返回值改为 NO
会发生什么呢?
2020-03-09 16:28:07.921634+0800 KVC简介[33560:1105250] 糊涂了吧,没有这个!
2020-03-09 16:28:07.921771+0800 KVC简介[33560:1105250] (null) - (null) - (null) - (null)
2020-03-09 16:28:07.921848+0800 KVC简介[33560:1105250] (null) - (null) - (null)
2020-03-09 16:28:07.921933+0800 KVC简介[33560:1105250] (null) - (null)
2020-03-09 16:28:07.922010+0800 KVC简介[33560:1105250] (null)
和我们猜想的一样,走了 setValue: forUndefinedKey:
方法,由此 步骤2 得到了验证。
接下来我们再将accessInstanceVariablesDirectly
返回值改为 YES
2020-03-09 16:29:49.127702+0800 KVC简介[33603:1110814] 帅帅金 - (null) - (null) - (null)
2020-03-09 16:29:49.127874+0800 KVC简介[33603:1110814] (null) - (null) - (null)
2020-03-09 16:29:49.127959+0800 KVC简介[33603:1110814] (null) - (null)
2020-03-09 16:29:49.128148+0800 KVC简介[33603:1110814] (null)
perfect,可以看到我们的值被赋值给 _name
,而其他的三个实例变量为空。如果我们将成员变量一一注释,就可以得出查找顺序依次为 _<key>
、_is<Key>
、<key>
、is<Key>
,至此 步骤3 也得到了完美的验证。
2、KVC取值过程
1、首先按照
get<key>
、<key>
、is<key>
、_<key>
的顺序 查找方法 ,如果找到方法,跳转到 步骤5 ,否则执行下一步。
2、如果没有找到上面的方法,KVC就会继续查找countOf<Key>
,objectIn<Key>AtIndex:
(对应NSArray的方法),<key>AtIndexes:
(对应NSArray 的objectsAtIndexes:
方法)格式的方法,判断是否是属于 NSArray。
3、查找countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
判断是否是属于 NSSet。
4、如果上述方法都不存在,判断对象的类方法accessInstanceVariablesDirectly
返回值,如果返回YES
则按顺序查找实例变量_<key>
、_is<Key>
、<key>
、is<Key>
,如果查询到符合条件的实例变量,会直接取出实例变量的值,然后进行到 步骤5。反之,直接到 步骤6。
5、 如果 步骤4 获取到的属性值是一个对象指针
,直接返回结果;
如果该值是NSNumber
支持的标量类型,将其存储为 NSNumber 类型的实例然后返回;
如果该值 不是NSNumber
支持的标量类型,将其转换为NSValue
对象然后返回。
6、调用valueForUndefinedKey:
方法进行报错。
实例验证:
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@property (nonatomic, strong) NSMutableArray *namesArrM;
@property (nonatomic, strong) NSMutableSet *namesSetM;
@property (nonatomic, strong) NSMutableOrderedSet *orderedSetM;
#pragma mark - getter
-(NSString *)getName {
return NSStringFromSelector(_cmd);
}
-(NSString *)name {
return NSStringFromSelector(_cmd);
}
-(NSString *)isName {
return NSStringFromSelector(_cmd);
}
-(NSString *)_name {
return NSStringFromSelector(_cmd);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"没找到");
return key;
}
//执行代码:
CHJManager *manager = [[CHJManager alloc] init];
manager->_name = @"_name";
manager->_isName = @"_isName";
manager->name = @"name";
manager->isName = @"isName";
NSLog(@"取值:%@",[manager valueForKey:@"name"]);
打印信息:
2020-03-09 16:33:00.970505+0800 KVC简介[35291:1326801] 取值:getName
通过打印信息,我们可以看到,首先是按 步骤1 所说的先 查找方法
如果在 步骤1 中没找到方法呢?
- (NSUInteger)countOfPens {
NSLog(@"- %s -", __func__);
return [self.array count];
}
- (id)objectInPensAtIndex:(NSUInteger)index {
NSLog(@"- %s -", __func__);
return self.array[index];
}
//验证代码
manager.array = @[@"pen0", @"pen1", @"pen2", @"pen3", @"pen4"];
NSArray *array = [manager valueForKey:@"pens"];
NSLog(@"%@", [array objectAtIndex:1]);
NSLog(@"数量 %ld", [array count]);
NSLog(@"是否存在该值 %d", [array containsObject:@"pen2"]);
这里我们对
countOf<key>
和objectIn<key>AtIndex:
方法进行重写,所以后面我们通过[manager valueForKey:@"pens"]
就可以获取到数组array
的值。
打印信息如下:
2020-03-10 10:20:12.953986+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954107+0800 KVC简介[5875:296832] pen1
2020-03-10 10:20:12.954185+0800 KVC简介[5875:296832] - -[CHJManager countOfPens] -
2020-03-10 10:20:12.954267+0800 KVC简介[5875:296832] 数量 5
2020-03-10 10:20:12.954343+0800 KVC简介[5875:296832] - -[CHJManager countOfPens] -
2020-03-10 10:20:12.954417+0800 KVC简介[5875:296832] - -[CHJManager countOfPens] -
2020-03-10 10:20:12.954492+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954574+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954647+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954721+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954851+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.955077+0800 KVC简介[5875:296832] 是否存在该值 1
看到这,步骤2 也得到了进一步的验证。步骤3 类似,所以这里不举例验证了。步骤4 又跟上面 设值 的 步骤2 一样,所以我们直接跳到 步骤5 来。
//指针类型
[manager setValue:@"CHJ" forKey:@"name"];
[manager setValue:@26 forKey:@"age"];
[manager setValue:@"小C" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[manager valueForKey:@"name"],[manager valueForKey:@"age"],[manager valueForKey:@"myName"]);
//NSNumber类型
[manager setValue:[NSNumber numberWithInt:26] forKey:@"age"];
NSLog(@"age:%@",[manager valueForKey:@"age"]);
//非NSNumber类型
ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[manager setValue:value forKey:@"threeFloats"];
NSValue *reslut = [manager valueForKey:@"threeFloats"];
NSLog(@"%@",reslut);
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
打印信息:
2020-03-10 10:57:00.003734+0800 KVC简介[6694:411097] CHJ - 26 - 小C
2020-03-10 10:57:00.004511+0800 KVC简介[6694:411097] age:26
2020-03-10 10:59:04.540073+0800 KVC简介[6694:411097] {length = 12, bytes = 0x0000803f0000004000004040}
2020-03-10 10:59:04.540182+0800 KVC简介[6694:411097] 1.000000 - 2.000000 - 3.000000
至此,步骤5 也得到了进一步的验证, 步骤6 比较常见,这里就不详细说明了。
3、KVC使用keyPath
在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以用KVC获取该类,然后再用KVC来获取这个自定义类的属性,但是这样比较繁琐,对此,KVC提供了一个解决方案,那就是键路径keyPath。顾名思义,就是按照路径寻找key。
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
实例验证如下:
Person *person = [[Person alloc] init];
person.name = @"帅帅金";
self.person = person;
NSString *str1 = self.person.name;
NSString *str2 = [self valueForKeyPath:@"person.name"];
NSLog(@"str1 == %@ str2 == %@",str1,str2);
[self setValue:@"飞机" forKeyPath:@"person.name"];
str1 = self.person.name;
str2 = [self valueForKeyPath:@"person.name"];
NSLog(@"str1 == %@ str2 == %@",str1,str2);
打印信息:
2020-03-10 09:30:02.340834+0800 KVC[4772:180246] str1 == 帅帅金 str2 == 帅帅金
2020-03-10 09:30:02.340974+0800 KVC[4772:180246] str1 == 飞机 str2 == 飞机
我们可以看到,这里的
name
是另外一个类的,当我们给这个自定义类的属性进行读取值的时候,我们就可以直接用keyPath
,是可以正常的输出。
如果我们不用 keyPath,只用key呢?
Person *person = [[Person alloc] init];
person.name = @"帅帅金";
self.person = person;
NSString *str1 = self.person.name;
NSString *str2 = [self valueForKey:@"person.name"];
NSLog(@"str1 == %@ str2 == %@",str1,str2);
[self setValue:@"飞机" forKey:@"person.name"];
str1 = self.person.name;
str2 = [self valueForKey:@"person.name"];
NSLog(@"str1 == %@ str2 == %@",str1,str2);
打印信息:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<ViewController 0x7ffda7f03f00> valueForUndefinedKey:]: this class is not key value coding-compliant for the key person.name.'
直接崩溃了,我们可以看到,因为直接使用的是key,就会把
person.name
整个当成 key 去寻找,找不到,会调用valueForUndefinedKey:
相关方法抛出异常。
小结: KVC对于 keyPath
的搜索机制第一步是分离key,用小数点来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。所以,当我们的属性或者实例变量是基本的系统类型就可以用 key
进行赋值和取值,但是属性或者实例变量也是另外一个类的时候,想要对该类的属性使用KVC进行赋值和取值时,用keyPath 更简便。
4、KVC处理异常
KVC处理值为 nil
异常
KVC中最常见的异常就是不小心使用了错误的 key,或者在设值中不小心传递了
nil
的值。不用方,KVC中有专门的方法来处理这些异常。
通常情况下,KVC不允许你要在调用setValue: forKey:
时对非对象传递一个nil
的值,因为值类型不能为nil
。如果你不小心传了,KVC会调用setNilValueForKey:
这个方法,抛出异常,所以一般而言最好还是重写这个方法。
@interface Test: NSObject {
NSUInteger age;
}
@end
@implementation Test
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能将%@设成nil", key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//Test生成对象
Test *test = [[Test alloc] init];
//通过KVC设值test的age
[test setValue:nil forKey:@"age"];
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
return 0;
}
打印信息:
2020-03-10 12:24:30.302134+0800 KVCKVO[35470:6258307] 不能将age设成nil
2020-03-10 12:24:30.302738+0800 KVCKVO[35470:6258307] test的年龄是0
KVC处理值为 UndefinedKey
异常
通常情况下,KVC不允许你要在调用setValue:属性值 forKey:(或者keyPath)时对不存在的key进行操作。 不然,会报错forUndefinedKey发生崩溃,重写forUndefinedKey方法避免崩溃。
@interface Test: NSObject {
}
@end
@implementation Test
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@", key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//Test生成对象
Test *test = [[Test alloc] init];
//通过KVC设值test的age
[test setValue:@10 forKey:@"age"];
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
return 0;
}
打印信息:
2020-03-10 12:30:18.564680+0800 KVCKVO[35487:6277523] 出现异常,该key不存在age
2020-03-10 12:30:18.565190+0800 KVCKVO[35487:6277523] 出现异常,该key不存在age
2020-03-10 12:30:18.565216+0800 KVCKVO[35487:6277523] test的年龄是(null)
网友评论