KVC
我们可以通过苹果官方文档看到KVC
的解释:
键值编码是
NSKeyValueCoding
非正式协议支持的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合键值编码时,可通过简洁,统一的消息传递接口通过字符串参数访问其属性,这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
通常,您使用访问器方法来访问对象的属性。一个
get
访问器(或getter
)从一个属性返回值,一个set
访问器(或setter
)给一个属性设置值。在Objective-C
中,您还可以直接访问属性的基础实例变量。以任何一种方式访问对象属性都很简单,但是需要调用特定于属性的方法或变量名。随着属性列表的增加或更改,访问这些属性的代码也必须如此。相反,与键值编码兼容的对象提供了一个简单的消息传递接口,该接口在其所有属性之间都是一致的。
键值编码是许多其他
Cocoa
技术的基础概念,例如键值观察,Cocoa
绑定,Core Data
和AppleScript-ability
。在某些情况下,键值编码还可以帮助简化代码。
在实现了访问器方法的类中,使用点语法和KVC
访问对象其实差别不大,二者可以任意混用。但是没有访问器方法的类中,点语法无法使用,这时KVC
就有优势了。
1、访问对象的属性
属性:这些是简单的值,例如标量,字符串或布尔值。值对象(例如NSNumber
)和其他不可变类型(例如NSColor
)也被视为属性。
一对一的关系:这些是具有自己属性的可变对象。对象的属性可以更改,而无需更改对象本身。例如,银行帐户对象可能具有所有者属性,该属性是Person
对象的实例,而Person
对象本身具有address
属性。所有者的地址可以更改,而无需更改银行帐户持有的所有者属性。
一对多关系:这些是集合对象。尽管也可以使用自定义集合类,但是通常使用NSArray
或NSSet
的实例来保存此类集合。
@interface YXPerson : NSObject
@property (nonatomic) NSNumber* accountNumber; // 一个属性
@property (nonatomic, strong) YXStudent *student; // 一对一的关系
@property (nonatomic) NSArray< Transaction* >* transactions; // 一对多的关系
@end
为了维护封装,对象通常为其接口上的属性提供访问器方法。 对象的作者可以显式地编写这些方法,也可以依靠编译器自动合成它们。 无论哪种方式,使用这些访问器之一的代码作者都必须在编译属性名称之前将其写入代码。 访问器方法的名称成为使用它的代码的静态部分。 例如:
// ✅ 在YXPerson.h中定义一个accountNumber属性
@interface YXPerson : NSObject
@property (nonatomic, assign) NSNumber * accountNumber;
@end
************************
// ✅ 在YXPerson.m中
- (NSNumber*) setAccountNumber{
return self. accountNumber;
}
************************
// ✅ 在使用的地方:
[person setAccountNumber:@19];
NSLog(@"accountNumber - %@",person. accountNumber);
************************
// ✅ 输出
2020-02-15 18:53:32.792859+0800 KVC简用[16886:179545] accountNumber - 19
这是最直接的,但缺乏灵活性。 另一方面,符合键值编码的对象提供了一种更通用的机制,可以使用字符串标识符访问对象的属性。
1.1、使用Key
和KeyPaths
径识别对象的属性
key
是标识特定属性的字符串。 通常,按照约定,代表属性的键是该属性本身在代码中出现的名称。key
必须使用ASCII
编码,不能包含空格,并且通常以小写字母开头(尽管有例外,例如在许多类中找到的URL
属性)。
由于上面代码中的YXPerson
类符合键值编码,因此它可以识别这些key
键的accountNumber
,student
和transactions
,这是其属性的名称。 您可以通过其键设置值,而不是调用setAccountNumber
:方法:
[person setValue:@19 forKey:@"accountNumber"];
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
,但是对象重写这个方法,以设置默认值或标记值。
例如:
YXPerson *person = [[YXPerson alloc] init];
// 1:Key-Value Coding (KVC) : 基本类型
[person setValue:@19 forKey:@"accountNumber"];
NSLog(@"person - %@",person.accountNumber);
// ✅ KeyPath赋值
YXStudent *student = [[YXStudent alloc] init];
person.student = student;
[person setValue:@"KP" forKeyPath:@"student.name"];
NSLog(@"student - %@ ",[person valueForKeyPath:@"student.name"]);
**********************
// ✅ 输出
2020-02-15 19:46:54.164662+0800 KVC简用[20478:221482] person - 19
2020-02-15 19:46:54.164921+0800 KVC简用[20478:221482] student - KP
2、访问集合属性
就像访问和设置其他属性一样,您也可以使用valueForKey:
和setValue:forKey:
来访问和设置集合属性的值。但是,当您要操纵这些集合的内容时,通常使用协议定义的可变代理方法最有效。
该协议定义了三种不同的代理对象访问代理方法,每种方法都有一个键和一个键路径变量:
-
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
它们返回行为类似于NSMutableArray
对象的代理对象。 -
mutableSetValueForKey:
和mutableSetValueForKeyPath:
它们返回行为类似于NSMutableSet
对象的代理对象。 -
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
它们返回行为类似于NSMutableOrderedSet
对象的代理象。
例如:
// 2: KVC - 集合类型
person.array = @[@"1",@"2",@"3"];
// ✅ 不可变数组
NSArray *ary = [person valueForKey:@"array"];
ary = @[@"100",@"2",@"3"];
[person setValue:ary forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
// ✅ 可变数组
NSMutableArray *muAry = [person mutableArrayValueForKey:@"array"];
muAry[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);
*************************
// ✅ 输出
2020-02-16 11:14:12.620737+0800 KVC简用[3041:31480] (
100,
2,
3
)
2020-02-16 11:14:12.623391+0800 KVC简用[3041:31480] (
200,
2,
3
)
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
消息发送到传入值对象以提取基础数据,然后存储该数据。
WX20200213-180313@2x.png
4.1、自定义结构体类型的转换
typedef struct {
float x, y, z;
} ThreeFloats;
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
通过KVC取值的时候,通过NSValue获取
NSValue* result = [myClass valueForKey:@"threeFloats"];
通过NSValue来进行包装一下,然后再通过KVC赋值。
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.判断是否是数组,如果是数组则查找
countOf<Key>
,objectIn<Key>AtIndex:
或<key>AtIndexes
,并返回一个新的数组。否则就执行步骤3。 - 3.判断是否是
NSSet
,查找countOf<Key>
,enumeratorOf<Key>
和memberOf<Key>:
。否则就执行步骤4。 - 4.调用
accessInstanceVariablesDirectly
方法,判断是否启用实例变量的查找,默认是YES
,也就是启用,当返回为YES
时,将按照这个_<key>
,_is<Key>
,<key>
, oris<Key>
,来一次查找。我们可以通过重写这个方法来禁用实例变量的查找。 - 5.如果检索到的属性值是对象指针,则只需返回结果。如果该值是
NSNumber
支持的标量类型,则将其存储在NSNumber
实例中并返回它。如果结果是NSNumber
不支持的标量类型,请转换为NSValue
对象并返回该对象。 - 6.如果所有的方法均失败,则调用
valueForUndefinedKey:
。 默认情况下,这会抛出一个异常,但是NSObject
的子类可以通过重写这个方法,来定制一些特性的功能。
验证:
第一点:
// ✅ YXPerson.h
@property (nonatomic, copy) NSString *name;
**********************
// ✅ YXPerson.m,可以每一次注释一个,看输出
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
**********************
// ✅ 取值
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
**********************
// ✅ 输出
2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:getName
2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:name
2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:isName
2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:_name
第二点:
// ✅ YXPerson.h
@property (nonatomic, strong) NSArray *array;
**********************
// ✅ YXPerson.m
- (NSInteger)countOfNames{
return self.array.count;
}
- (id)objectInNamesAtIndex:(NSUInteger)index{
return _array[index];
}
**********************
// ✅ 取值
person.array = @[@"1",@"2",@"3"];
NSLog(@"%@",[person valueForKey:@"names"]);
**********************
// ✅ 输出
2020-02-16 13:04:10.290894+0800 KVC简用[10284:112668] (
1,
2,
3
)
第三点:
// ✅ YXPerson.h
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSSet *set;
**********************
// ✅ YXPerson.m
// 个数
- (NSUInteger)countOfBooks{
NSLog(@"%s",__func__);
return [self.set count];
}
// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
NSLog(@"%s",__func__);
return [self.set containsObject:object] ? object : nil;
}
// 迭代器
- (id)enumeratorOfBooks {
// objectEnumerator
NSLog(@"来了 迭代编译");
return [self.array reverseObjectEnumerator];
}
**********************
// ✅ 取值
person.array = @[@"pen0", @"pen1", @"pen2", @"pen3"];
// set 集合
person.set = [NSSet setWithArray:person.array];
NSSet *set = [person valueForKey:@"books"];
[set enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"set遍历 %@",obj);
}];
**********************
// ✅ 输出
2020-02-16 13:14:56.213050+0800 KVC简用[11040:122217] -[YXPerson countOfBooks]
2020-02-16 13:14:56.213228+0800 KVC简用[11040:122217] -[YXPerson countOfBooks]
2020-02-16 13:14:56.213361+0800 KVC简用[11040:122217] 来了 迭代编译
2020-02-16 13:14:56.213494+0800 KVC简用[11040:122217] set遍历 pen3
2020-02-16 13:14:56.213610+0800 KVC简用[11040:122217] set遍历 pen2
2020-02-16 13:14:56.213720+0800 KVC简用[11040:122217] set遍历 pen1
2020-02-16 13:14:56.213825+0800 KVC简用[11040:122217] set遍历 pen0
5.2、setter
方法查找
setValue:forKey:
的默认实现(给定键和值参数作为输入),尝试将名为key
的属性设置为value
,在使用这个方法设置值时,对象的内部会经历以下流程。
-
1.按该顺序查找名为
set <Key>:
、_set <Key>
或setIs<Key>
的第一个访问器。 如果找到,请使用输入值调用它并完成。 -
2.如果没有找到
setter
访问器,并且类方法accessInstanceVariablesDirectly
返回YES
,则按该顺序查找名称类似于_ <key>
,_ is <Key>
,<key>
或is <Key>
的实例变量。 如果找到,直接用输入值设置变量并完成操作。 -
3.在找不到访问器或实例变量后,调用
setValue:forUndefinedKey:
。 默认情况下,这会抛出一个异常,但是NSObject
的子类可以通过重写这个方法来提供特定的操作。
验证:
第一点:
// ✅ YXPerson.h
@property (nonatomic, copy) NSString *name;
**********************
// ✅ YXPerson.m
//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
**********************
// ✅ 取值
[person setValue:@"YX" forKey:@"name"];
**********************
// ✅ 输出
2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson setName:] - YX
2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson _setName:] - YX
2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson setIsName:] - YX
第二点:
// ✅ YXPerson.h
@interface YXPerson : NSObject{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
**********************
// ✅ YXPerson.m
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"%@ is Undefined", key);
}
**********************
// ✅ 取值
[person setValue:@"YX" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
NSLog(@"%@-%@",person->name,person->isName);
NSLog(@"%@",person->isName);
**********************
// ✅ 1. 四个成员变量_name,_isName,name,isName
2020-02-16 13:33:03.731309+0800 KVC简用[12265:138431] YX-(null)-(null)-(null)
2020-02-16 13:33:03.731481+0800 KVC简用[12265:138431] (null)-(null)-(null)
2020-02-16 13:33:03.731596+0800 KVC简用[12265:138431] (null)-(null)
2020-02-16 13:33:03.731693+0800 KVC简用[12265:138431] (null)
// ✅ 2. 三个成员变量_isName,name,isName
2020-02-16 13:35:21.183230+0800 KVC简用[12445:141103] YX-(null)-(null)
2020-02-16 13:35:21.183438+0800 KVC简用[12445:141103] (null)-(null)
2020-02-16 13:35:21.183590+0800 KVC简用[12445:141103] (null)
// ✅ 3. 两个成员变量name,isName
2020-02-16 13:36:00.207210+0800 KVC简用[12502:141939] YX-(null)
2020-02-16 13:36:00.207396+0800 KVC简用[12502:141939] (null)
// ✅ 4. 一个成员变量isName
2020-02-16 13:36:51.657983+0800 KVC简用[12575:142914] YX
**********************
// ✅ 四个成员变量都不在
2020-02-16 13:37:58.726567+0800 KVC简用[12670:144203] name is Undefined