简介
KVC
的全称是Key-Value Coding
,翻译成中文是键值编码
,键值编码是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象是键值编码兼容的对象时,可以通过简洁,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
常用API
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;
//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC 设值、取值流程探索
这里主要依据官网描述和代码调试配合完成探索。
-
setValue:forKey:
将给定的key和value参数作为输入,尝试在接收调用的对象中将名为key的属性的值设置为value。设置步骤如下:
【第一步】查找是否存在指定的setter方法,查找顺序是:
1.set<Key>:
2._set<Key>:
3.setIs<Key>:
这三个方法,若存在其中任意一个,则执行该方法,在该方法中可以设置属性的值,若不存在,则进入【第二步】。
注意:KVO设值过程中不会查找和调用_setIs<Key>
函数。
【第二步】检查类方法InstanceVariablesDirectly
的返回值。
YES:查看是否有指定的成员变量,查找的顺序是:
1._<Key>
2._is<Key>
3.<Key>
4.is<Key>
若存在其中任意一个,则向那个成员变量赋值。若不存在则进入【第三步】。
NO:则进入【第三步】
【第三步】执行setValue:forUndefinedKey:
方法,该方法一般情况是抛出NSUndefinedKeyException
类型的异常。可以在应用中重写该方法。
KVO设值流程如下图所示:
-
valueForKey:
在给定key参数作为输入的情况下,尝试从接收调用的对象中获取名为key的属性的值。获取值步骤如下:
【第一步】查找是否存在指定的getter方法,查找顺序是:
1.get<Key>
2.<Key>
3.is<Key>
4._<Key>
如果找到这4个方法中的一个,则调用它并进入【第五步】。否则,继续下一步。
【第二步】查找countOf<Key>
以及objectIn<Key>AtIndex:
和<Key>AtIndexes:
方法。如果找到
countOf<Key>
和其它两个方法中的一个,则会返回一个可以响应NSArray所有方法
的代理集合
(它是NSKeyValueArray
,是NSArray的子类)。或者说给这个代理集合
发送属于NSArray的方法,就会以countOf<Key>
,objectIn<Key>AtIndex:
或<Key>AtIndexes:
这几个方法组合的形式调用。还有一个可选的get<Key>:range:
方法。所以你想重新定义KVC的一些功能,你可以添加这些方法。如果不能返回
代理集合
,即未找到指定的方法,请继续进入【第三步】。【第三步】同时查找
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
这三个方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法
的代理集合
,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
组合的形式调用。【第四步】如果还没有找到,检查类方法
InstanceVariablesDirectly
的返回值。
如果为YES,则查找指定的成员变量,如果找到指定的成员变量,则返回该成员变量。查找顺序为:
1._<Key>
2._is<Key>
3.<Key>
4.is<Key>
如果为NO,则进入【第六步】【第五步】根据搜索到的属性值类型,返回不同的结果:
* 如果是对象,则直接返回。
* 如果是NSNumber支持的标量类型
,则将其包装成NSNumber对象
并返回它。
* 如果是NSNumber不支持的标量类型
,则将其转换为NSValue对象
并返回它。【第六步】执行
valueForUndefinedKey:
方法,该方法一般情况是抛出NSUndefinedKeyException
类型的异常。可以在应用中重写该方法。
自定义实现KVC
自定义 KVC 设值
既然已经知道KVC的设值流程,那接下来就模仿系统接口,自定义一个KVC设值接口hq_setValue:forKey:
。
- 异常判断,当key为空时,则不继续操作。
- 由于KVC设值是先查找
setter
方法,因此,先根据查找的顺序定义setter
方法名称。 - 通过
respondsToSelector
函数判断当前方法是否存在,若存在,则执行方法并在执行后退出接口。 - 若
setter
方法不存在,判断是否响应accessInstanceVariablesDirectly方法,即间接访问实例变量,返回YES,继续下一步设值;返回NO,则抛出异常。 - 间接访问变量赋值(只会走一次),顺序是:_key、_isKey、key、isKey。
- 定义一个收集实例变量的可变数组,通过
class_copyIvarList
获取对象中ivar列表
,通过ivar_getName
,获取ivar名称
。 - 通过
class_getInstanceVariable
方法,获取相应的ivar
- 通过
object_setIvar
方法,对相应的ivar
设置值
- 定义一个收集实例变量的可变数组,通过
- 如果找不到相关实例变量,则抛出异常。
hq_setValue:forKey:
代码如下:
- (void)hq_setValue:(id)value forKey:(NSString *)key{
if (key == nil || key.length == 0) {
return;
}
//将key值的首字符变成大写,为后续补充set做准备
NSString* Key = [key capitalizedString];
//拼接setKey方法名
NSString* setKey = [[NSString alloc] initWithFormat:@"set%@:", Key];
//拼接_setKey方法名
NSString* _setKey = [[NSString alloc] initWithFormat:@"_set%@:", Key];
//拼接setIsKey方法名
NSString* setIsKey = [[NSString alloc] initWithFormat:@"setIs%@:", Key];
//依次执行上面的三个方法
BOOL result = [self hq_performSelectorWithMethodName:setKey value:value];
if(result){
return ;
}
result = [self hq_performSelectorWithMethodName:_setKey value:value];
if(result){
return ;
}
result = [self hq_performSelectorWithMethodName:setIsKey value:value];
if(result){
return ;
}
//若未找到setter方法,则需要判断 accessInstanceVariablesDirectly 这个值
result = [self.class accessInstanceVariablesDirectly];
if(!result){
@throw [NSException exceptionWithName:@"HQUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
//开始查找成员变量
NSArray* ivalArr = [self getIvarListName];
//_<key> _is<Key> <key> is<Key>
NSString* _key = [NSString stringWithFormat:@"_%@", key];
NSString* _isKey = [NSString stringWithFormat:@"_is%@", Key];
NSString* isKey = [NSString stringWithFormat:@"is%@", Key];
if([ivalArr containsObject:_key]){
Ivar ivar = class_getInstanceVariable(self.class, _key.UTF8String);
object_setIvar(self, ivar, value);
return ;
}
else if([ivalArr containsObject:_isKey]){
Ivar ivar = class_getInstanceVariable(self.class, _isKey.UTF8String);
object_setIvar(self, ivar, value);
return ;
}
else if([ivalArr containsObject:key]){
Ivar ivar = class_getInstanceVariable(self.class, key.UTF8String);
object_setIvar(self, ivar, value);
return ;
}
else if([ivalArr containsObject:isKey]){
Ivar ivar = class_getInstanceVariable(self.class, isKey.UTF8String);
object_setIvar(self, ivar, value);
return ;
}
//如果找不到相关实例,则抛出异常
@throw [NSException exceptionWithName:@"HQUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
自定义 KVC 取值
异常判断,当key为空时,则不继续操作。
查找是否存在getter方法,按照查找顺序定义
getter
方法的方法名称,查找顺序:get<Key> --> <Key> --> is<Key> --> _<key>若
getter
方法未找到,此时针对集合类型做了一些特殊处理。查找是否存在countOf<Key>
方法
3.1 若存在countOf<Key>
方法
3.1.1 查找是否存在objectIn<Key>AtIndex:
或者<Key>AtIndexes:
3.1.1.1 若存在,则执行objectIn<Key>AtIndex:
或<Key>AtIndexes:
3.1.1.2 若不存在,则进入3.1.2
3.1.2 查找是否存在enumeratorOf<Key>:
并且存在memberOf<Key>:
3.1.2.1 若存在,则执行enumeratorOf<Key>:
和memberOf<Key>:
3.1.2.2 若不存在,则进入步骤4
3.2 若不存在countOf<Key>
方法,则进入步骤4检查
accessInstanceVariablesDirectly
这个布尔值
若为YES,则查找成员变量,查找流程:_<Key> --> _is<Key> --> <Key> --> is<Key>
若为NO,则直接进入步骤5-
若是以上都查找失败,则执行 valueForUndefinedKey: 方法,应用可重写该方法
hq_setValue:forKey:
代码如下:
- (id)hq_valueForKey:(NSString *)key{
if (key == nil || key.length == 0) {
return nil;
}
//查找getter方法 get<Key> --> <key> --> is<Key> --> _<key>
NSString* Key = [key capitalizedString];
NSString* getKey = [NSString stringWithFormat:@"get%@", Key];
NSString* isKey = [NSString stringWithFormat:@"is%@", Key];
NSString* _key = [NSString stringWithFormat:@"_%@", key];
id value = [self hq_performSelectorWithMethodName:getKey];
if(value){
return value;
}
value = [self hq_performSelectorWithMethodName:key];
if (value) {
return value;
}
value = [self hq_performSelectorWithMethodName:isKey];
if (value) {
return value;
}
value = [self hq_performSelectorWithMethodName:_key];
if (value) {
return value;
}
//查找countOfKey:方法 + objectIn<Key>AtIndex: 或者 <Key>AtIndexes:
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
NSString *keyAtIndexes = [NSString stringWithFormat:@"%@AtIndexes:", key];
NSString *enumeratorOfKey = [NSString stringWithFormat:@"enumeratorOf%@",Key];
NSString *memberOfKey = [NSString stringWithFormat:@"memberOf%@:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
BOOL result = [self respondsToSelector:NSSelectorFromString(countOfKey)];
if(result){
[self performSelector:NSSelectorFromString(countOfKey)];
if([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]){//countOfKey + objectInKeyAtIndex
int count = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray* retArr = [[NSMutableArray alloc] init];
for (int index = 0; index<count; index++) {
IMP imp = class_getMethodImplementation(self.class, NSSelectorFromString(objectInKeyAtIndex));
id (*func)(id, SEL, int) = (void*)imp;
id obj = func(self, NSSelectorFromString(objectInKeyAtIndex),index);
[retArr addObject:obj];
}
return retArr;
}
if([self respondsToSelector:NSSelectorFromString(keyAtIndexes)]){//countOfKey + keyAtIndexes
int count = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray* retArr = [[NSMutableArray alloc] init];
for (int index = 0; index<count; index++) {
NSIndexSet* indexes = [[NSIndexSet alloc] initWithIndex:index];
id obj = [self performSelector:NSSelectorFromString(keyAtIndexes) withObject:indexes];
[retArr addObjectsFromArray:obj];
}
return retArr;
}
if ([self respondsToSelector:NSSelectorFromString(enumeratorOfKey)] && [self respondsToSelector:NSSelectorFromString(memberOfKey)]) {
[self performSelector:NSSelectorFromString(countOfKey)];
id enumerator = [self hq_performSelectorWithMethodName:enumeratorOfKey];
if(enumerator){
return [[NSSet alloc] initWithArray:[enumerator allObjects]];
}
}
}
#pragma clang diagnostic pop
//若未找到setter方法,则需要判断 accessInstanceVariablesDirectly 这个值
result = [self.class accessInstanceVariablesDirectly];
if(!result){
@throw [NSException exceptionWithName:@"HQUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
//开始查找成员变量
NSArray* ivalArr = [self getIvarListName];
NSLog(@"ivalArr:%@", ivalArr);
// _<Key> --> _is<Key> --> <Key> --> is<Key>
NSString* _isKey = [NSString stringWithFormat:@"_is%@", Key];
if([ivalArr containsObject:_key]){
Ivar ivar = class_getInstanceVariable(self.class, _key.UTF8String);
return object_getIvar(self, ivar);
}
else if([ivalArr containsObject:_isKey]){
Ivar ivar = class_getInstanceVariable(self.class, _isKey.UTF8String);
return object_getIvar(self, ivar);
}
else if([ivalArr containsObject:key]){
Ivar ivar = class_getInstanceVariable(self.class, key.UTF8String);
return object_getIvar(self, ivar);
}
else if ([ivalArr containsObject:isKey]){
Ivar ivar = class_getInstanceVariable(self.class, isKey.UTF8String);
return object_getIvar(self, ivar);
}
@throw [NSException exceptionWithName:@"HQUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
return @"";
}
KVC 运算符
@avg
当指定@avg
运算符时,valueForKeyPath:
将会读取集合中每个元素,并将其转换为double(用0代替nil值)
,计算这些值的算术平均值
。然后,返回存储在NSNumber对象
中的结果。@count
指定@count
运算符时,valueForKeyPath:
返回集合对象
中的对象数
。@max
当指定@max
操作符时,valueForKeyPath:
将搜索集合中各项,并返回最大
的那个值。@min
当指定@min
操作符时,valueForKeyPath:
将搜索集合中各项,并返回最小
的那个值。@sum
当指定@sum
运算符时,valueForKeyPath:
将会读取集合中每个元素,并将其转换为double(用0代替nil值)
,计算这些值的总和
。然后,返回存储在NSNumber对象
中的结果。@distinctUnionOfObjects
当您指定@distinctUnionOfObjects
运算符时,valueForKeyPath:
创建并返回一个数组,该数组包含与右键路径指定的属性
相对应的集合的不同对象
。并且会省略重复值
。@unionOfObjects
当您指定@unionOfObjects
运算符时,valueForKeyPath:
创建并返回一个数组,其中包含与右键路径指定的属性
相对应的集合的所有对象
。与@distinctUnionOfObjects
不同,不会删除重复的对象
。@distinctUnionOfArrays
当您指定@distinctUnionOfArrays
运算符时,valueForKeyPath:
创建并返回一个数组,该数组包含与右键路径指定的属性
相对应的所有集合的组合
的不同对象
。并且会省略重复项
。@unionOfArrays
当您指定@unionOfArrays
运算符时,将valueForKeyPath:
创建并返回一个数组,该数组包含与由右键路径指定的属性
相对应的所有集合的组合
的所有对象
,而不会删除重复项
。-
@distinctUnionOfSets
当您指定@distinctUnionOfSets
运算符时,将valueForKeyPath:
创建并返回一个NSSet对象
,该对象包含与右键路径指定的属性
相对应的所有集合的组合
中的不同对象
。该运算符的行为类似于@distinctUnionOfArrays
,只是它期望NSSet
包含NSSet对象实例而不是NSArray对象实例。以上运算符如何使用请查看文末Demo。
KVC异常处理
找不到key
在设值/取值时,经过了KVC的一系列步骤之后,仍然找不到key,此时就会抛出异常。对于这种情况,可以重写以下方法:
设值:setValue:forUndefinedKey:
取值:valueForUndefinedKey:
在上面两个方法重写一些逻辑,使得找不到key程序不会发生崩溃,但是一般不太建议这样。当值为nil时
在setValue:forKey:
时,当value为nil
时,程序会根据当前对象是否支持nil
而抛出异常,若对象支持nil,则不会发生崩溃但是错误很隐蔽,不容易被发现。若对象不支持nil,则会发生崩溃。
对于这种情况,当value为nil
时,调用setValue:forKey:
时,KVC会调用setNilValueForKey:
方法,因此可以在应用中重写该方法,从而避免程序崩溃问题。
KVC应用场景
- 动态的设值和取值
- 通过
setValue:forKey:
和valueForKey:
方法进行设值、取值。 - 通过
setValue:forKeyPath:
和valueForKeyPath:
方法进行设值、取值。
- 通过
- 通过KVC访问和修改私有变量
在日常开发中,对于类的私有属性
,在外部定义的对象,是无法直接访问私有属性
的,但是对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性
。 - 多值操作
model和字典的转换可以通过下面两个KVC的API实现//字典转模型 - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; //模型转字典 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- 修改一些系统空间的内部属性
在日常开发中,我们知道,很多UI控件都是在其内部由多个UI空间组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是自定义tabbar
、个性化UITextField中
的placeHolderText
。 - 用KVC实现高阶消息传递
在对容器类
使用KVC时,valueForKey:
将会被传递给容器中的每一个对象
,而不是对容器本身进行操作,结果会被添加到返回的容器
中,这样,可以很方便的操作集合来返回另一个集合。
Demo
自定义KVC见Demo源码