KVC(Key-Value Coding)
键值编码,由NSKeyValueCoding
非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合kvc时,可以通过简介,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。就是指iOS的开发中,可以允许开发者通过key
变量名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。
KVC是许多其他Cocoa技术的基础概念,例如 KVO
,Cocoa bindings
, Core Data
, 和AppleScript-ability
。
在OC中,对象从NSObject
(直接或间接)继承时,通常都是实现了NSKeyValueCoding
协议,又为基本方法提供默认方法的实现,例如可以通过valueForKey:
方法和setValue:forKey:
方法来分别获取参数的值和设置参数的值。
接下来为了更好地对KVC的说明,定义了一个User的对象
User.h的代码:
@interface User : NSObject{
@public
NSString *name;
NSString *isName;
NSString *_name;
NSString *_isName;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
KVC的Getter查找模式
通过valueForKey:方法给定一个key
参数作为输入的默认实现,它的调用的类实例内部执行过程(其中key为键字符串,相当于成员变量
名):
1.首先搜索实例的getter方法,按照get<Key>
,<key>
,is<Key>
,_<key>
这些get方法来查找的,如果找到则调用它并执行步骤5。否则继续下一步。
2.如果没有找到步骤1中的简单访问器方法,那么会在实例中搜索countOf<Key>
和objectIn<Key>AtIndex:
相关的两个方法是否有实现,如果有找到这两个方法,则会以一个NSArray的数组形式返回,如果找不到就继续执行步骤3
3.如果以上两个步骤都没有找到的话,就会寻找三个方法,分别是countOf<Key>
,enumeratorOf<Key>
和memberOf<Key>:
,如果这个三个方法找到的话,就可以创建一个NSSet的集合代理对象返回,此代理对象随后将其收到的任何NSSet消息转换为countOf <Key>,enumeratorOf <Key>和memberOf <Key>:
消息的某种组合,以创建它的对象。 实际上,代理对象与与键值编码兼容的对象一起使用,类型于这个对象就像是NSSet一样,否则执行步骤4.
4.执行到这个步骤的时候,会先找到类方法accessInstanceVariablesDirectly,如果返回YES(默认是yes),就会搜索实例的变量名_<key>,_is<Key>,<key>,is<Key>
这种顺序来查找。如果找到,直接获取实例变量的值,然后继续执行步骤5,否则执行步骤6.
5.如果检索到属性值是对象指针,则返回结果。如果该值是NSNumber类型,则将其存储为NSNumber类型并返回,如果不是则转换为NSValue对象并返回对象。
6.如果以上的所有方法都查找不到,这时会调用valueForUndefinedKey:
,默认情况下会引发NSUndefinedKeyException异常,但是可以在NSObject的子类中重写这个方法来做一些处理防止程序闪退。
根据上面的步骤来验证getter的查找模式
以下是部分代码的实现:
User *user = [[User alloc] init];
user.arr = @[@"name0", @"name1", @"name2", @"name3"];
[user setValue:@"张三" forKey:@"name"];
NSString *name = [user valueForKey:@"name"];
NSLog(@"name的值:%@",name);
在User.m的代码里面
-(NSString *)getName{
return @"getName";
}
-(NSString *)name{
return @"name";
}
-(NSString *)isName{
return @"isName";
}
-(NSString *)_name{
return @"_name";
}
通过执行代码并且按照步骤1中的顺序来对部分代码注释可以发现get方法的执行结果分别是
[8389:179602] name的值:我是getName方法
[8450:181614] name的值:我是name方法
[8476:182555] name的值:我是isName方法
[8503:183568] name的值:我是_name方法
所以通过以上的结果知道getter的执行顺序是getName()-->name()-->isName()-->_name()。
为了对步骤2,步骤3和步骤4进行验证在User.m中添加了如下代码:
// 个数
- (NSUInteger)countOfName{
NSLog(@"%s",__func__);
return [self.arr count];;
}
//// 获取值
- (id)objectInNameAtIndex:(NSUInteger)index {
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"name %lu", index];
}
// 是否包含这个成员对象
- (id)memberOfName:(id)object {
NSLog(@"%s",__func__);
return [self.set containsObject:object] ? object : nil;
}
// 迭代器
- (id)enumeratorOfName {
// objectEnumerator
NSLog(@"来了 迭代编译");
return [self.arr reverseObjectEnumerator];
}
+(BOOL)accessInstanceVariablesDirectly{
return YES;
}
通过注释掉getter里面的方法,会发现是先执行countOfName
和objectInNameAtIndex
这两个方法的,执行结果如下:
[8727:189400] -[User countOfName]
[8727:189400] -[User countOfName]
[8727:189400] -[User objectInNameAtIndex:]
[8727:189400] -[User objectInNameAtIndex:]
[8727:189400] -[User objectInNameAtIndex:]
[8727:189400] -[User objectInNameAtIndex:]
[8727:189400] name的值:(
"name 0",
"name 1",
"name 2",
"name 3"
)
当将objectInNameAtIndex
这个方法注释掉之后,就会执行memberOfName
和enumeratorOfName
。当然如果这些执行的步骤中有某一个方法没有实现的话,都会直接走到下一个步骤的,所以这些方法是缺一不可的。
当对步骤4验证的时候就需要将countOfName``objectInNameAtIndex``memberOfName
和enumeratorOfName
等方法注释掉,并且User的实现方法改为:
User *user = [[User alloc] init];
[user setValue:@"张三" forKey:@"name"];
NSString *name = [user valueForKey:@"name"];
NSLog(@"name的值:%@",name);
NSLog(@"获取到的值:_name:%@,_isName:%@,name:%@,isName:%@",
user->_name,user->_isName,user->name,user->isName);
NSLog(@"获取到的值:_isName:%@,name:%@,isName:%@",user->_isName,user->name,user->_isName);
NSLog(@"获取到的值:name:%@,isName:%@",user->name,user->_isName);
NSLog(@"获取到的值:_isName:%@",user->_isName);
User.h的部分代码:
@interface User : NSObject{
@public
NSString *name;
NSString *isName;
NSString *_name;
NSString *_isName;
}
从而得到在accessInstanceVariablesDirectly
返回YES的时候(默认是YES)查找的顺序是_name-->_isName-->name-->isName,当accessInstanceVariablesDirectly
为NO的时候直接就报valueForUndefinedKey
的NSUndefinedKeyException
异常,当然也可以直接在User里面实现valueForUndefinedKey
对异常做处理。到此就结束了KVC对成员变量的查找的验证。
KVC的Setter的查找模式
相对于getter
的查找模式来说,setter
的查找模式就简单很多了。
通过setValue:forKey:,给定的key
和value
作为参数输入,其搜索的是以下步骤:
1.先按set<Key>
,_set<Key>
这个顺序来查找方法,如果找到就设置值做相应的操作
2.如果没有找到简单的访问方法,先找到类方法accessInstanceVariablesDirectly
,如果返回YES(默认返回yes),查找实例变量的名称,按照_<key>
,_is<Key>
,<key>
,is<Key>
这个顺序来查找,如果找到就可以设置变量并完成操作
3.在以上两种步骤都没有实现的情况下,就会调用setValue:forUndefinedKey:
,默认情况下会引发异常,但是NSObject的子类可以实现这个方法来防止报异常。
这里就不做一一验证了,与上面的Getter是差不多的。
KVC的keyPath
在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径
keyPath
。
用到的两个方法
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
简单的用法,例如定义一个Student类
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@end
然后这个Student在User类中作为属性,实现为:
User *user = [[User alloc] init];
Student *student = [[Student alloc] init];
user.student = student;
[user setValue:@"学生" forKeyPath:@"student.name"];
NSLog(@"%@",[user valueForKeyPath:@"student.name"]);
这就是keyPath的简单用法
设置非对象值为nil
在KVC中设置非对象值为nil的时候,例如:
Student *student = [[Student alloc] init];
[student setValue:nil forKey:@"age"];
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<Student 0x6000005273c0> setNilValueForKey]: could not set nil as the value for the key age.'
这是会报NSInvalidArgumentException
异常,主要实现了-(void)setNilValueForKey:(NSString *)key
方法就不会有异常了,也可以在该方法中做一些处理。但是如果对Student中的name属性设置为nil的时候[student setValue:nil forKey:@"name"];
却不会报错,这是为什么呢?通过去看这个方法的说明
/* Given that an invocation of -setValue:forKey:
would be unable to set the keyed value because the type of the parameter of the corresponding
accessor method is an NSNumber scalar type or NSValue structure type
but the value is nil, set the keyed value
using some other mechanism.
The default implementation of this method raises an NSInvalidArgumentException.
You can override it to map nil values to
something meaningful in the context of your application.
*/
- (void)setNilValueForKey:(NSString *)key;
了解到这里大概的说法就是只对NSNumber和NSValue的结构体等类型的数据在setvalue的时候为nil才会报错。
KVC访问非对象值
在User类中添加一个结构体,并且这个结构体是User类中的一个属性
typedef struct{
float x, y, z;
} ThreeFloats;
如果类似一般情况下直接设置值的话,是直接就报错的了
在上文中有提到,在Getter的查找模式中,步骤5中讲到,如果查找值,找到的是NSNumber类型的就以NSNumber的返回,但是如果例如类似结构体,CGSize,CGRect等这些类型的,只能转换为NSValue的形式设置值,当然对结构体的获取值也是需要转换的。
User *user = [[User alloc] init];
ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[user setValue:value forKey:@"threeFloats"];
NSValue *reslut = [user valueForKey:@"threeFloats"];
NSLog(@"%@",reslut);
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
[68376:588080] {length = 12, bytes = 0x0000803f0000004000004040}
[68376:588080] 1.000000 - 2.000000 - 3.000000
至此有关kvc的常用的介绍就到这里了,当然这只是简单地介绍了部分有关kvc的内容,还有更多的有关可变数组,可变集合和可变有序集等搜索模式可以去苹果官网键值编码编程指南去了解更多。