导语:
KVC(Key-value coding)键值编码,允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值,不需要调用明确的存取方法。通过这个技术就可以在运行时动态地访问和修改对象的属性,而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多iOS开发技巧都是基于KVC实现的。
Demo源码见Github上的工程KVCDemo,主要从以下几个方面来展开KVC:
- KVC定义
- KVC寻找key策略
- KVC使用keyPath
- KVC处理异常
- KVC键值验证
- KVC处理容器类属性
KVC定义
KVC的定义是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,在文件NSKeyValueCoding.h中
@interface NSObject(NSKeyValueCoding)
所以对于所有继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject),以下是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类别中还有其他的一些比较重要方法,如下
// 默认返回YES,表示如果没有找到set(get)方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索。
+ (BOOL)accessInstanceVariablesDirectly;
// 如果Key不存在,且无法搜索到任何和Key有关的字段或者属性,则最后才会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 和方法valueForUndefinedKey:一样,但这个方法是设值的时候才调用
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 如果调用setValue方法时给Value传nil,则会调用这个方法。
- (void)setNilValueForKey:(NSString *)key;
// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 集合操作的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回,这个用于KVC监听属性为NSMutableArray*类型。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
KVC寻找key策略
一般开发过程中,怎么用KVC大部分开发都比较清楚,就那么几个常用方法调用就可以,但是不是很清楚key的寻找策略,通过Demo来验证下key的寻找顺序。
设值
设值会调用方法setValue:forKey:,设置方法代码底层的执行机制大致步骤如下流程图:- 程序优先调用set
:或_set 方法,代码通过setter方法完成设置。 注意,这里的
是指成员变量名,首字母大小写要符合KVC的命名规则,下同 - 如果没有找到set
:方法,KVC机制会搜索该类里面有没有名为 的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以 命名的变量,KVC都可以对该成员变量赋值。 - 如果该类即没有set
:方法,也没有_ 成员变量,KVC机制会搜索_is 的成员变量。 - 和上面一样,如果该类即没有set
:方法,也没有_ 和_is 成员变量,KVC机制再会继续搜索 和is 的成员变量。再给它们赋值。 - 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
简单说KVC机制在设值的时候会按照set
顺序搜索成员并进行赋值操作,但是如果开发者重写了类方法+ (BOOL)accessInstanceVarialbesDirectly
并且让其返回NO,这样在搜索的时候会直接从步骤1跳转到步骤5。
KVC机制设值顺序效果可以看KVCDemo中对KVCModel的各个name设值,然后查看下KVCmodel的对象信息:
@interface KVCModel ()
{
NSString* _name;
BaseModel* _name2;
NSString* _isName2;
NSString* _isName3;
NSString* name3;
NSString* name4;
NSString* isName4;
NSString* isName5;
}
@end
@implementation KVCModel
- (void)setName:(NSString*)name
{
NSLog(@"%s name=%@", __FUNCTION__, name);
_name = name;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
NSLog(@"%s value=%@, 该key=%@不存在!", __FUNCTION__, value, key);
}
- (id)valueForUndefinedKey:(NSString *)key
{
NSLog(@"%s,该key不存在%@", __FUNCTION__, key);
return nil;
}
调用设值如下:
KVCModel* model = [KVCModel new];
[model setValue:@"newName" forKey:@"name"];
[model setValue:@"newName2" forKey:@"name2"];
[model setValue:@"newName3" forKey:@"name3"];
[model setValue:@"newName4" forKey:@"name4"];
[model setValue:@"newName5" forKey:@"name5"];
[model setValue:@"newName6" forKey:@"name6"];
打断点查看model的信息,如下:
同时日志输出如下:
2018-11-28 19:32:36.482351+0800 KVCDemo[46228:1428290] -[KVCModel setName:] name=newName
2018-11-28 19:32:36.482508+0800 KVCDemo[46228:1428290] -[KVCModel setValue:forUndefinedKey:] value=newName6, 该key=name6不存在!
重写类方法accessInstanceVariablesDirectly并返会NO,效果可以查看KVCNoAccessInstanceModel的逻辑处理:
对应日志输出如下:
2018-11-28 19:38:03.325658+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel setName:] name=newName
2018-11-28 19:38:03.325795+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel setValue:forUndefinedKey:] value=newKey, 该key=key不存在!
2018-11-28 19:38:03.325924+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel valueForUndefinedKey:],该key不存在name
2018-11-28 19:38:03.326002+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel getKey]
注意:在KVCModel中_name2定义成自定义类BaseModel,但是通过方法setValue:forKey:设置NSString*,打断点会发现_name2类型已经变为NSString*,本来BaseModel有个方法获取属性num值,在设值后去调用会出现crash。还有一种情况是如果name类型为基本数据类型(BOOL,int等),类型是不会改变。
取值
当调用valueForKey:方法时,KVC对key的搜索顺序有点不同于setValue:forKey:方法,大致步骤如下:- 按顺序查找方法
get
,如果其中一种方法找到直接调用,如果是BOOL或者int等值类型,会将包装成NSNumber对象。, , is - 如果步骤1的几个getter方法都没有找到,KVC机制会查找是否实现了方法
countOf
,同时还实现了两个方法(objectIn
和AtIndex
)中的一个即可。如果都实现就会返回一个可以响应NSArray所有方法的代理集合(它是 NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOfAtIndexes ,objectIn AtIndex或 AtIndexes这几个方法组合的形式调用,在这几个函数打断点查看会发现还有一个可选方法get :range:。所以想重新定义KVC的一些功能,可以添加这些方法,添加的时候注意方法名要符合KVC标准命名方法。 - 步骤2没有找到,就会同时查找
countOf
格式的方法。如果这三个方法都找到,就会返回一个可以响应NSSet所有方法的代理集合,同步骤2一样的形式调用。,enumeratorOf ,memberOf - 步骤3也没找到就会检查类方法+ (BOOL)accessInstanceVarialbesDirectly,如果返回是YES的,和设值一样的顺序,按
_
搜索成员变量名,当然这种方法不推荐这么做,这样直接访问实例变量破坏了封装性,使代码更脆弱。这样还没找到就会调用》_is 》 》is valueForUndefinedKey:
方法,默认抛出异常。
步骤2的效果可以看KVCModel中实现的方法:
#pragma mark - 当key使用name6时,KVC会找到这两个方法。
- (NSUInteger)countOfName7
{
NSLog(@"%s enter", __FUNCTION__);
return 4;
}
-(id)objectInName7AtIndex:(NSUInteger)index
{
NSLog(@"%s enter", __FUNCTION__);
return @(index * 2);
}
其他情况搜索逻辑可以见KVCDemo对KVCModel数据获取,输出日志如下:
2018-11-28 20:09:28.786934+0800 KVCDemo[51488:1543594] key=name value=newName
2018-11-28 20:09:28.787015+0800 KVCDemo[51488:1543594] -[KVCModel name2] enter
2018-11-28 20:09:28.787074+0800 KVCDemo[51488:1543594] key=name2 value=(null)
2018-11-28 20:09:28.787175+0800 KVCDemo[51488:1543594] key=name3 value=newName3
2018-11-28 20:09:28.787301+0800 KVCDemo[51488:1543594] key=name4 value=newName4
2018-11-28 20:09:28.787419+0800 KVCDemo[51488:1543594] key=name5 value=newName5
2018-11-28 20:09:28.787519+0800 KVCDemo[51488:1543594] -[KVCModel valueForUndefinedKey:],该key不存在name6
2018-11-28 20:09:28.787580+0800 KVCDemo[51488:1543594] key=name6 value=(null)
2018-11-28 20:09:28.787719+0800 KVCDemo[51488:1543594] -[KVCModel countOfName7] enter
2018-11-28 20:09:28.787778+0800 KVCDemo[51488:1543594] -[KVCModel countOfName7] enter
2018-11-28 20:09:28.787836+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
2018-11-28 20:09:28.787882+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
2018-11-28 20:09:28.787959+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
2018-11-28 20:09:28.788027+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
2018-11-28 20:09:28.788120+0800 KVCDemo[51488:1543594] key=name7 value=(
0,
2,
4,
6
)
KVC使用keyPath
类的成员变量有可能是自定义类或其他复杂数据类型,对这种成员变量可以先用KVC获取该属性,然后再用KVC来获取这个自定义类的属性,这样一层层去获取,但这样比较繁琐。对此KVC提供一个解决方案,就是键路径keyPath,顾名思义就是按照路径寻找key。主要有两个以下两个方法:
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
代码实现见对KVCModel对象逻辑处理:
NSLog(@"keyPath=baseModel.num value=%@", [model valueForKeyPath:@"baseModel.num"]);
[model setValue:@"newNumValue" forKeyPath:@"baseModel.num"];
NSLog(@"keyPath=baseModel.num value=%@", [model valueForKeyPath:@"baseModel.num"]);
KVC对于keyPath的搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照上面介绍的顺序搜索。
KVC处理异常
使用KVC过程中最常见的异常就是不小心使用了错误的key,或者在设值中不小心传了nil的值,KVC有专门的方法处理这些异常。
-
KVC处理nil异常
,如果在设值过程中,不小心传了nil,KVC会调用方法setNilValueForKey:,这个默认方法是抛出异常,所以一般而言最好重写这个方法。 -
KVC处理UndefinedKey异常
,如果在设值取值传的key不存在,这样的操作就会crash,设值会调用到setValue:forUndefinedKey:方法,而取值会调用valueForUndefinedKey:方法,这两个方法默认都是抛出异常,所以最好重写这两个方法来规避crash。
KVC键值验证
KVC提供了验证key对应的value是否可用的方法:
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
方法实现会取探索类是否实现了方法
-(BOOL)validateName8:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
如果有实现这个方法,就用实现的方法返回,没有就直接返回YES,这是默认返回结果,效果可以看KVCDemo中对属性name8的验证。
注意:
键值验证不会主动去做验证,需要开发者手动去验证,所以即使类实现了验证方法,但是KVC也不会主动验证,还是会设值成功。当然也有一些技术,比如CoreData会自动调用。
KVC处理容器类属性
对象的属性可以是一对一的,也可以是一对多的。一对多的属性要么是有序的数组,要么就是无序的集合。
不可变的有序数组属性(NSArray)和无序集合(NSSet)属性可使用方法valueForKey:
来获取。而当对象的属性是可变的容器时,对于有序的容器,可以用以下方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
该方法返回一个可变有序数组。对于无序的容器,可以用以下方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
该方法返回一个可变的无序集合。同时他们也有对应的keyPath版本:
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
可变的容器获取方法,一般在KVO对容器监听的时候会用到,具体使用可见《iOS KVO原理探究》
当NSDictionary对象使用KVC时,valueForKey:的表现行为和objectForKey:一样,使用valueForKeyPath:可访问多层嵌套的字典会方便点,在KVC中有两个关于NSDictionary的方法:
// 输入一组key,返回这组key对应的属性,再组成一个字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
// 用来修改Model中对应key的属性
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
结尾
KVC作为在iOS开发中的利器,基于运行时的编程方式极大提高了灵活性,简化了代码,可以实现一些难以想象的功能,是许多iOS开发黑魔法的基础,一般有以下几个场景:
动态的取值和设值
访问和修改私有变量
修改一些控件的内部属性
操作容器类(NSArray和NSSet等)
-
实现高阶消息传递
当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作,结果会添加进返回的容器中,这样开发就很方便操作集合来返回另一个集合,如以下代码:
NSArray* languageArrStr = @[@"english", @"franch", @"chinese"];
NSArray* languageArrCapStr = [languageArrStr valueForKey:@"capitalizedString"];
for (NSString* languageStr in languageArrCapStr)
{
NSLog(@"%@", languageStr);
}
打印结果:
2018-11-29 14:13:07.940471+0800 KVCDemo[17618:2982517] English
2018-11-29 14:13:07.940554+0800 KVCDemo[17618:2982517] Franch
2018-11-29 14:13:07.940694+0800 KVCDemo[17618:2982517] Chinese
方法 capitalizedString被传递到NSArray的每一项,这样NSArray的每一员都会执行capitalizedString并返回一个包含结果的 新NSArray,可从打印结果看出成功的把首字符都转成大写。