1. KVC 的定义
键/值编码(Key-value coding,KVC) 可以允许开发者通过Key名访问对象的属性或给对象的属性赋值, 而不需要调用明确的存取方法,并有一组api供开发者使用,像操作字典一样操作对象属性/成员变量/关联对象。
这样就可以在 运行时动态地访问和修改 对象的属性。而不是在编译时确定。这种机制不属于Objective-C语言的特性,而是Cocoa提供的一种特性。
通过定义一个NSObject
的类别NSKeyValueCoding
来实现KVC功能。因此所有继承了NSObject
的类都支持KVC。NSKeyValueCoding
的四个重要方法:
1 | - (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值 |
NSKeyValueCoding
还有其它许多方法,我列举一些,详细可查看官方文档 NSKeyValueCoding:
1 | + (BOOL)accessInstanceVariablesDirectly; |
2 KVC是如何寻找Key
2.1 设置值
当调用setValue:forKey:
方法来设置属性值时,执行机制如下:
- 先调用setter方法
set<Key>:属性值
- 如果没有找到setter方法,KVC就会检测
+ (BOOL)accessInstanceVariablesDirectly
的返回值,是默认值YES
,就按照_<key>
,_isKey
,key
,isKey
的顺序一一查找。只要存在_<key>
,无论该变量是在类接口处定义,还是在类实现处定义,也不管是什么访问修饰符,KVC都可以对其访问。 - 如果没有setter方法,也没找到
_<key>
,_isKey
,key
,isKey
中的任何一个,KVC就会执行方法- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
,默认是抛出异常。
代码示例(andyRon/KVCDemo1):
1 | @interface Dog : NSObject |
打印结果:
1 | KVCDemo1[5107:12399654] 设置值出现异常,key为:name的变量不存在 |
重写+(BOOL)accessInstanceVariablesDirectly
方法让其返回NO后,KVC机制就不会实现,就直接调用- (nullable id)valueForUndefinedKey:(NSString *)key;
或- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
稍微修改以下代码,看示例(andyRon/KVCDemo2):
1 | @interface Dog : NSObject |
打印结果:
1 | KVCDemo2[5323:12426199] newNameValue |
虽然+(BOOL)accessInstanceVariablesDirectly
方法结果还是NO,但因为有了setter和getter方法就不会出现异常了。
当+(BOOL)accessInstanceVariablesDirectly
结果为YES,的🌰代码我就不列出了,可查看andyRon/KVCDemo3。
2.2 KVC取值
对于取值方法valueForKey:
, KVC对key
的查询方式不同于setValue:forKey:
,如下:
首先按
get<Key>
,<key>
,is<Key>
的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL
或者Int
等值类型, 会将其包装成一个NSNumber
对象。如果getter没有找到,KVC则会查找
countOf<Key>
,objectIn<Key>AtIndex
或<Key>AtIndexes
格式的方法。如果有一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray
,是NSArray
的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray
的方法,就会以countOf<Key>
,objectIn<Key>AtIndex
或<Key>AtIndexes
这几个方法组合的形式调用。还有一个可选的get<Key>:range:
方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。如果上面的方法没有找到,那么会同时查找
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
组合的形式调用。
在类自定义了KVC的实现,并且实现了上面的方法,就可以将返回的对象当数组(NSArray)用了
- 如果还没有找到,再检查类方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>
,_is<Key>
,<key>
,is<Key>
的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly
返回NO的话,那么会直接调用valueForUndefinedKey:
示例代码(andyRon/KVCDemo4):
1 | @interface TwoTimesArray : NSObject |
结果:1
2
3
4
5
6KVCDemo4[25723:3947658] 10
KVCDemo4[25723:3947658] NSKeyValueArray
KVCDemo4[25723:3947658] 0:0 1:2 2:4 3:6
KVCDemo4[25723:3947658] 1
KVCDemo4[25723:3947658] 2
KVCDemo4[25723:3947658] newName
3 KVC中使用keyPath
一个类的属性可能是另外一个类,可以通过keyPath方式获取或设置这种多层中属性,这种解决方式也是通过NSKeyValueCoding
中的方法来实现的。
1 | //通过KeyPath来取值 |
来看看具体代码例子(KVCKeyPathDemo):
1 | @interface Address : NSObject |
打印结果:
1 | KVCKeyPathDemo[6330:12568821] country1:China country2:China |
keyPath中,key之间用.
分隔,当keyPath出现错误时,就会调用valueForUndefinedKey:key
方法。
4 KVC的异常处理
两种情况,一种是key
或keyPath
错误,上面也都提到过,就是调用valueForUndefinedKey:key
方法。
另一种情况是在使用setValue:forKey:
方法时值设置为nil
了,这是不被允许的,会调用setNilValueForKey:
方法。
1 | @implementation People |
1 | [people setValue:nil forKey:@"age"]; |
5 KVC处理非对象和自定义对象
valueForKey:
总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber
或者NSValue
对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开以者需要手动转换成原来的类型。尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:
却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。
对于自定义对象,KVC也会正确地设值和取值。因为传递进去和取出来的都是id类型,所以需要开发者自己担保类型的正确性,运行时Objective-C在发送消息的会检查类型,如果错误会直接抛出异常。
6 KVC和容器类
对象的属性可以是一对一的,也可以是一对多的。一对多的属性要么是有序的(数组),要么是无序的(集合)。
不可变的有序容器属性(NSArray)和无序容器属性(NSSet)一般可以使用valueForKey:
来获取。但也可以利用更灵活的方法来管理,比如:- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
实例代码(KVCDemo6):
1 | @interface Demo : NSObject |
结果:
1 | KVCDemo6[11393:3534594] ( |
当只是普通地调用[_arr addObject:@"addItem"];
时,Observer并不会回调,只有[[self mutableArrayValueForKey:@"arr"] addObject:@"addItemObserver"];
这样写时才能正确地触发KVO。
对于无序容器属性(NSSet)有对应的方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
另外还有对应的keyPath
方法:
1 | - (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath; |
7 KVC和字典
KVC与字典相关的方法:
1 | - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; |
示例代码(KVCDemo7):
1 | @interface Address : NSObject |
结果:
1 | KVCDemo7[14135:3606256] { |
8 KVC的应用场景
动态取值和设值
用KVC来访问和修改私有变量
Model和字典转换
修改一些控件的内部属性
有的时候可以通过KVC修改一些苹果官方没有公开的属性,比如UITextField
中的placeHolderText
。这个时候用playground能很方便的演示(KVCDemo8):
没有公开的属性可通过runtime的方式获取(KVCDemo9):
1 | import UIKit |
操作集合
用KVC实现高阶消息传递
用KVC中的函数操作集合
最后
参照前辈的文章 iOS开发技巧系列—详解KVC(我告诉你KVC的一切)
学习KVC,动手写了各种简单的示例加深理解,由于目前KVC实际项目中运用的还不是很多,有很多地方理解的还不够透彻。
示例代码: andyRon/KVCDemo
参考:
iOS开发技巧系列—详解KVC(我告诉你KVC的一切)
Key-Value Coding Programming Guide - Apple Developer