什么是KVC?
KVC的全称叫Key-Value Coding,也叫做键值编码,在apple官方文档中是这么解释的。
键值编码是NSKeyValueCoding非正式协议支持的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合键值编码时,可通过简洁,统一的消息传递接口通过字符串参数访问其属性,这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
通常,您使用访问器方法来访问对象的属性。一个get访问器(或getter)从一个属性返回值,一个set访问器(或setter)给一个属性设置值。在Objective-C中,您还可以直接访问属性的基础实例变量。以任何一种方式访问对象属性都很简单,但是需要调用特定于属性的方法或变量名。随着属性列表的增加或更改,访问这些属性的代码也必须如此。相反,与键值编码兼容的对象提供了一个简单的消息传递接口,该接口在其所有属性之间都是一致的。
键值编码是许多其他Cocoa技术的基础概念,例如键值观察,Cocoa绑定,Core Data和AppleScript-ability。在某些情况下,键值编码还可以帮助简化代码。
1、访问对象的属性
- 属性:这些是简单的值,例如标量,字符串或布尔值。值对象(例如NSNumber)和其他不可变类型(例如NSColor)也被视为属性。
- 一对一的关系:这些是具有自己属性的可变对象。对象的属性可以更改,而无需更改对象本身。例如,银行帐户对象可能具有所有者属性,该属性是Person对象的实例,而Person对象本身具有address属性。所有者的地址可以更改,而无需更改银行帐户持有的所有者属性。
- 一对多关系:这些是集合对象。尽管也可以使用自定义集合类,但是通常使用NSArray或NSSet的实例来保存此类集合。
@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance; // 一个属性
@property (nonatomic) Person* owner; // 一对一的关系
@property (nonatomic) NSArray< Transaction* >* transactions; // 一对多的关系
@end
为了维护封装,对象通常为其接口上的属性提供访问器方法。 对象的作者可以显式地编写这些方法,也可以依靠编译器自动合成它们。 无论哪种方式,使用这些访问器之一的代码作者都必须在编译属性名称之前将其写入代码。 访问器方法的名称成为使用它的代码的静态部分。 例如,上述代码中银行行帐户对象,编译器将合成一个可以为myAccount实例调用的设置器:
[myAccount setCurrentBalance:@(100.0)];
这是最直接的,但缺乏灵活性。 另一方面,符合键值编码的对象提供了一种更通用的机制,可以使用字符串标识符访问对象的属性。
1.1、使用Key和KeyPaths径识别对象的属性
key是标识特定属性的字符串。 通常,按照约定,代表属性的键是该属性本身在代码中出现的名称。 key必须使用ASCII编码,不能包含空格,并且通常以小写字母开头(尽管有例外,例如在许多类中找到的URL属性)。
由于上面代码中的BankAccount
类符合键值编码,因此它可以识别这些key键的owner
,currentBalance
和transactions
,这是其属性的名称。 您可以通过其键设置值,而不是调用setCurrentBalance:
方法:
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
1.2、使用key获取属性的值
当对象采用NSKeyValueCoding
协议时,它符合键值编码。继承自NSObject
的对象(提供了该协议的基本方法的默认实现)会自动采用具有某些默认行为的该协议。这样的对象至少实现以下基于键的基本getter:
valueForKey:-返回由key参数命名的属性的值。如果根据访问者搜索模式中描述的规则找不到由关键字命名的属性,则该对象将向自身发送
valueForUndefinedKey:
消息。valueForUndefinedKey:
的默认实现抛出了NSUndefinedKeyException
,但是子类可以重写此行为并更优雅地处理这种情况。valueForKeyPath:-返回相对于接收者的指定密钥路径的值。密钥路径序列中不符合特定键的键值编码的任何对象(即
valueForKey:
的默认实现无法找到访问器方法)都接收到valueForUndefinedKey:
消息。dictionaryWithValuesForKeys:-返回相对于接收者的键数组的值。该方法为数组中的每个键调用
valueForKey:
。返回的NSDictionary
包含数组中所有键的值。
集合对象(例如
NSArray
,NSSet
和NSDictionary
)不能包含nil
作为值。 而是使用NSNull
对象表示nil
值。NSNull
提供了单个实例,表示对象属性的nil
值。dictionaryWithValuesForKeys:
和相关的setValuesForKeysWithDictionary:
的默认实现会自动在NSNull
(在dictionary
参数中)和nil
(在存储的属性中)之间转换。
1.3、使用key设置属性的值
与getter
一样,与键值编码兼容的对象还根据NSObject
中提供的NSKeyValueCoding
协议的实现,为一小组具有默认行为的定义setter:
setValue:forKey:
-将相对于接收消息的对象的指定键的值设置为给定值。setValue:forKey:
的默认实现:将表示标量和结构的NSNumber
和NSValue
对象自动解包,并将它们分配给属性。如果该对象没有对应的key的属性,则该对象将向自身发送
setValue:forUndefinedKey:
消息。setValue:forUndefinedKey:
的默认实现抛出一个NSUndefinedKeyException
。但是,子类可以重写此方法以自定义方式处理请求。setValue:forKeyPath:
-在相对于接收者的指定键路径处设置给定值。key路径序列中不符合特定键的键值编码的任何对象都会收到setValue:forUndefinedKey:
消息。setValuesForKeysWithDictionary:
-使用字典键标识属性,使用指定字典中的值设置接收器的属性。默认实现为每个键值对调用setValue:forKey :
,并根据需要用nil
代替NSNull
对象。
在默认实现中,当您尝试将非对象属性设置为nil
值时,符合键值编码的对象会向自身发送setNilValueForKey:
消息。 setNilValueForKey:
的默认实现抛出NSInvalidArgumentException
,但是对象重写这个方法,以设置默认值或标记值。
2、访问集合属性
就像访问和设置其他属性一样,您也可以使用valueForKey:
和setValue:forKey:
来访问和设置集合属性的值。但是,当您要操纵这些集合的内容时,通常使用协议定义的可变代理方法最有效。
该协议定义了三种不同的代理对象访问代理方法,每种方法都有一个键和一个键路径变量:
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
它们返回行为类似于NSMutableArray
对象的代理对象。mutableSetValueForKey:
和mutableSetValueForKeyPath:
它们返回行为类似于NSMutableSet
对象的代理对象。mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
它们返回行为类似于NSMutableOrderedSet
对象的代理象。
3、集合运算符
当您发送与键值编码兼容的对象valueForKeyPath:
消息时,可以将集合运算符嵌入到键路径中。集合运算符是一小部分关键字之一,其后带有一个at符号(@),该符号指定getter
在返回数据之前应执行的操作以某种方式处理数据。由NSObject
提供的valueForKeyPath:
的默认实现会实现此行为。
-
@avg.属性名
求集合中对象某个属性的平均值。 -
@count
求集合中对象个数。 -
@max.属性名
求集合中对象某个属性的最大值。 -
@min.属性名
求集合中对象某个属性的最小值。 -
@sum.属性名
求集合中对象某个属性的和。 -
@distinctUnionOfObjects.属性名
取出集合中所有对象某个属性的值,并将这些值存入一个新的数组并返回。这个操作去重。 -
@unionOfObjects.属性名
取出集合中所有对象某个属性的值,并将这些值存入一个新的数组并返回。这个操作不去重。 -
@distinctUnionOfArrays.属性名
取出嵌套集合(集合嵌套集合)中所有对象某个属性的值,并返回一个新的数组。这个操作去重。 -
@unionOfArrays.属性名
取出嵌套集合(集合嵌套集合)中所有对象某个属性的值,并返回一个新的数组。这个操作不去重。 -
@distinctUnionOfSets.属性名
返回值是个一个NSSet
效果和distinctUnionOfArrays
一样。
4、类型转换
当您调用协议的一种getters,例如valueForKey:
时,默认实现将根据访问者搜索模式中描述的规则来确定为指定键提供值的特定访问器方法或实例变量。 如果返回值不是对象,则getter使用此值初始化NSNumber
对象(用于标量)或NSValue
对象(用于结构体),并返回该值。
类似地,默认情况下,使用setValue:forKey
之类的setter:在给定特定键的情况下,确定属性的访问器或实例变量所需的数据类型。 如果数据类型不是对象,则设置器首先将适当的<type> Value
消息发送到传入值对象以提取基础数据,然后存储该数据。
Data type | Creation method | Accessor method |
---|---|---|
BOOL |
numberWithBool: |
boolValue (in iOS) charValue (in macOS)* |
char |
numberWithChar: |
charValue |
double |
numberWithDouble: |
doubleValue |
float |
numberWithFloat: |
floatValue |
int |
numberWithInt: |
intValue |
long |
numberWithLong: |
longValue |
long long |
numberWithLongLong: |
longLongValue |
short |
numberWithShort: |
shortValue |
unsigned char |
numberWithUnsignedChar: |
unsignedChar |
unsigned int |
numberWithUnsignedInt: |
unsignedInt |
unsigned long |
numberWithUnsignedLong: |
unsignedLong |
unsigned long long |
numberWithUnsignedLongLong: |
unsignedLongLong |
unsigned short |
numberWithUnsignedShort: |
unsignedShort |
Data type | Creation method | Accessor method |
---|---|---|
NSPoint |
valueWithPoint: |
pointValue |
NSRange |
valueWithRange: |
rangeValue |
NSRect |
valueWithRect: (macOS only). |
rectValue |
NSSize |
valueWithSize: |
sizeValue |
4.1、自定义结构体类型的转换
typedef struct {
float x, y, z;
} ThreeFloats;
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
取值
NSValue* result = [myClass valueForKey:@"threeFloats"];
设置值
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
5、KVC的底层原理
5.1、基本getter的搜索模式
当一个对象调用valueForKey:
方法取值的时候,他的内部执行以下过程。
- 1.在实例中搜索找到具有名称的第一个访问器方法
get<Key>,<key>,is<Key>,或者_<key>,
按照这个顺序。如果找到,则调用它并执行步骤5。否则,请继续下一步。 - 2.判断是否是数组,如果是数组则对数组中的每个对象一次调用
valueForKey:
方法,并返回一个新的数组。否则就执行步骤3。 - 3.判断是否是
NSSet
,如果是集合则对集合中的每个对象一次调用valueForKey:
方法,并返回一个新的集合。否则就执行步骤4。 - 4.调用
accessInstanceVariablesDirectly
方法,判断是否启用实例变量的查找,默认是YES
,也就是启用,当返回为YES
时,将按照这个_<key>, _is<Key>, <key>, or is<Key>,
来一次查找。我们可以通过重写这个方法来禁用实例变量的查找。 - 5.如果检索到的属性值是对象指针,则只需返回结果。如果该值是
NSNumber
支持的标量类型,则将其存储在NSNumber
实例中并返回它。如果结果是NSNumber
不支持的标量类型,请转换为NSValue
对象并返回该对象。 - 6.如果所有的方法均失败,则调用
valueForUndefinedKey:
。 默认情况下,这会抛出一个异常,但是NSObject
的子类可以通过重写这个方法,来定制一些特性的功能。
5.1、基本setter的搜索模式
setValue:forKey:
的默认实现(给定键和值参数作为输入),尝试将名为key的属性设置为value,在使用这个方法设置值时,对象的内部会经历以下流程。
1.按该顺序查找名为
set <Key>:或_set <Key>
的第一个访问器。 如果找到,请使用输入值调用它并完成。2.如果没有找到setter访问器,并且类方法
accessInstanceVariablesDirectly
返回YES,则按该顺序查找名称类似于_ <key>,_ is <Key>,<key>或is <Key>
的实例变量。 如果找到,直接用输入值设置变量并完成操作。3.在找不到访问器或实例变量后,调用
setValue:forUndefinedKey:
。 默认情况下,这会抛出一个异常,但是NSObject的子类可以通过重写这个方法来提供特定的操作。