KVC苹果官方文档链接:Key-Value Coding Programming Guide
KVC(Key-Value Coding)键值编码是一种通过NSKeyValueCoding非正式的协议,间接访问对象的属性的机制。
在iOS开发中,开发者可以通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取(getter和setter)方法。这样就可以在运行时动态地访问和修改对象的属性。
KVC的定义是对NSObject的扩展来实现的,在Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型的类,其对象都能使用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类别中还有其他的一些方法
1. +(BOOL)accessInstanceVariablesDirectly; //默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
2. -(BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; //KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
3. -(NSMutableArray *)mutableArrayValueForKey:(NSString *)key; //这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
4. -(nullable id)valueForUndefinedKey:(NSString *)key; //如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
5. -(void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; //和上一个方法一样,但这个方法是设值。
6. -(void)setNilValueForKey:(NSString *)key; //如果你在SetValue方法时面给Value传nil,则会调用这个方法
7. -(NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
KVC是如何寻找key的
我们来探讨一下KVC在内部是按什么样的顺序来寻找key的,相信大家经常使用- (void)setValue:(nullable id)value forKey:(NSString *)key这个方法,
当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制如下:
1、 首先调用setter方法,setter方法有两个(setKey:和setIsKey:)。
其中Key表示属性名,首字母大写,两个方法执行顺序为先通过key找setKey:方法,如果没有找到,则去找setIsKey:方法。
2、如果没有找到setter方法,则去检查下面的方法返回值
+ (BOOL)accessInstanceVariablesDirectly
默认该方法会返回YES,表示可以直接访问实例的成员变量。
如果你重写返回NO,则不可以访问实例的成员变量,KVC会执行setValue:forUndefinedKey:方法(控制台报错,没有找到对应的key),一般开发者不会重写该方法返回NO。
接下来我们来看一下,返回YES的情况下,是如何查找key。找成员变量 先后顺序是:_key -> _isKey -> key ->isKey
先去查找是否有_key的变量,如果没有则去查找是否有_isKey的变量,如果没有则去查找是否有key的变量,如果还没有则去查找是否有isKey的变量,isKey没有后则会执行setValue:forUndefinedKey:抛出异常。如果以上四个变量找到其中任何一个,KVC都可以对该成员变量赋值。
#import "Person.h"
@implementation Person
{
/**
在没有找到对应的setter方法后,会先看+(BOOL)accessInstanceVariablesDirectly的返回值
如果返回YES,则查找key,返回NO,执行setValue:forUndefinedKey:抛异常
KVC中找key的优先级:_name → _isName → name → isName
如果这几个成员变量都没有,执行setValue:forUndefinedKey:抛出异常
*/
NSString *_name;
NSString *_isName;
NSString *isName;
NSString *name;
}
/**
1、 首先调用setter方法,setter方法有两个(setKey:和setIsKey:)。
其中Key表示属性名,首字母大写,两个方法执行顺序为先通过key找setKey:方法,如果没有找到,则去找setIsKey:方法。
2、如果没有找到setter方法,则去检查下面的方法返回值
+ (BOOL)accessInstanceVariablesDirectly
默认该方法会返回YES,表示可以直接访问实例的成员变量。
如果你重写返回NO,则不可以访问实例的成员变量,KVC会执行setValue:forUndefinedKey:方法(控制台报错,没有找到对应的key),一般开发者不会重写该方法返回NO。
*/
-(void)setName:(NSString *)name{
_name = name;
NSLog(@"-(void)setName:(NSString *)name被执行了");
}
-(void)setIsName:(NSString *)name{
_name = name;
NSLog(@"-(void)setIsName:(NSString *)name被执行了");
}
+(BOOL)accessInstanceVariablesDirectly{
return NO;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"赋值抛出异常,没有找到key");
}
- (NSString *)description
{
return [NSString stringWithFormat:@"_name = %@ _isName = %@, name = %@, isName = %@", _name, _isName, name, isName];
}
我们来验证一下赋值时寻找key的顺序,首先重写+(BOOL)accessInstanceVariablesDirectly返回NO,注释掉-(void)setName:(NSString )name和-(void)setIsName:(NSString )name两个方法,运行后出现如下错误
如果取消注释,赋值成功。
在没有对应的setter方法,+(BOOL)accessInstanceVariablesDirectly返回YES时,运行结果如下
可以看到优先对_name赋值,在这里你可以注释掉 _name,看接下来会优先查找哪个变量,验证后你会发现顺序是按照我们上面说的那样:KVC中找key的优先级:_name → _isName → name → isName
当调用valueforKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:
1、首先按getKey→key→isKey的顺序查找getter方法,找到的话会直接执行。如果是BOOL或者int等值类型, 会做NSNumber转换。
2、如果还没有找到,再检查类方法
+ (BOOL)accessInstanceVariablesDirectly,如果返回YES,那么和先前的设值一样,会按_key -> _isKey -> key ->isKey顺序搜索成员变量名,如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
3、最后还没有找到成员变量的话,调用valueForUndefinedKey:抛出异常
在KVC中使用KeyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
在开发过程中,一个类的成员变量有可能是其他的自定义类,通过键路径KeyPath,可以很容易做到。
例如:Person类中有一个Dog类的对象,修改dog的颜色color
Person *per = [[Person alloc] init];
per.dog = [[Dog alloc] init];
[per setValue:@"黑色" forKeyPath:@"dog.color"];//赋值color成黑色
NSLog(@"%@", [per valueForKeyPath:@"dog.color"]);//输出color值
自定义实现KVC赋值机制
弄清楚KVC如何寻找key,按照寻找顺序,可以自己实现KVC,下面的代码是自定义的赋值
#import "NSObject+LRKVC.h"
#import <objc/runtime.h>
@implementation NSObject (LRKVC)
-(void)lr_setValue:(id)value forKey:(NSString *)key{
//key值需要合法
if (key == nil || key.length == 0) {
return;
}
//value类型:如果不是id类型,抛异常
if (![value isKindOfClass:[NSObject class]]) {
NSException *exception = [NSException exceptionWithName:@"LRKVO" reason:@"must be NSObject type" userInfo:nil];
@throw exception;
return;
}
//调用相关setter方法
NSString *setKey = [NSString stringWithFormat:@"set%@:", key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(setKey)]) {
[self performSelector:NSSelectorFromString(setKey) withObject:value];
return;
}
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(setIsKey)]) {
[self performSelector:NSSelectorFromString(setIsKey) withObject:value];
return;
}
//异常处理方法
if (![self.class accessInstanceVariablesDirectly]) {
NSException *exception = [NSException exceptionWithName:@"LRKVO" reason:@"你返回了一个NO" userInfo:nil];
@throw exception;
}
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
//_key
for (int i = 0; i < count; i++) {
Ivar ivar =ivars[i];
NSString *keyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@", key]]) {
object_setIvar(self, ivar, value);
free(ivars);
return;
}
}
//_isKey
for (int i = 0; i < count; i++) {
Ivar ivar =ivars[i];
NSString *keyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_is%@", key.capitalizedString]]) {
object_setIvar(self, ivar, value);
free(ivars);
return;
}
}
//key
for (int i = 0; i < count; i++) {
Ivar ivar =ivars[i];
NSString *keyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
if ([keyName isEqualToString:key]) {
object_setIvar(self, ivar, value);
free(ivars);
return;
}
}
//isKey
for (int i = 0; i < count; i++) {
Ivar ivar =ivars[i];
NSString *keyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@", key.capitalizedString]]) {
object_setIvar(self, ivar, value);
free(ivars);
return;
}
}
//异常处理方法
[self valueForUndefinedKey:key];
}