KVC
全名:Key-value coding
,中文简直编码。苹果对其定义如下:
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.
键-值编码是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码时,可以通过简洁、统一的消息传递接口通过字符串参数对其属性进行寻址。这种间接访问机制补充了实例变量及其关联访问器方法提供的直接访问。
一、使用场景
KVC
在我们的平常开发中应该是高频使用的,接下来我们先简单看一下常用的KVC
使用场景:
1.基本类型
LPPerson *person = [[LGPerson alloc] init];
// 一般setter 方法
person.name = @"name"; // setter -- llvm
person.age = 18;
person->myName = @"name";
NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);
// 非正式协议 - 间接访问
[person setValue:@"KC" forKey:@"name"];
2.集合类型
person.array = @[@"1",@"2",@"3"];
// 修改数组
// person.array[0] = @"100";
// 第一种:搞一个新的数组 - KVC 赋值就OK
NSArray *array = [person valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
// 第二种
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);
3.访问非对象属性
typedef struct {
float x, y, z;
} ThreeFloats;
ThreeFloats floats = {1.,2.,3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *value1 = [person valueForKey:@"threeFloats"];
NSLog(@"%@",value1);
ThreeFloats th;
[value1 getValue:&th];
NSLog(@"%f-%f-%f",th.x,th.y,th.z);
4.key-path
LPStudent *student = [LPStudent alloc];
student.subject = @"火箭班";
person.student = student;
[person setValue:@"Swift" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
二、KVC
原理
既然要了解KVC
的原理,那么苹果官方的文档必定是不二之选,[传送门]。(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/SearchImplementation.html#//apple_ref/doc/uid/20000955-CJBBBFFA)
我们先看下setter
和getter
:
1.setter
Search Pattern for the Basic Setter
The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:
1.Look for the first accessor named set: or _set , in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.
2.If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _, _is , , or is , in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
3.Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
搜索基本Setter的模式
setValue:forKey:
的默认实现,给定键和值参数作为输入,尝试在接收调用的对象内部设置一个名为key
的属性为value
(或者,对于非对象属性,为value
的解包装版本,如描述的非对象值),使用以下过程:
1.按照顺序查找第一个访问器set
或: _set
。如果找到,使用输入值(或根据需要打开包装值)和finish
调用它。
2.如果没有找到简单的访问器,并且类方法accessinstancevariables
直接返回YES
,那么查找一个实例变量,其名称如下:_
,_is< key>
,,或
is< key>
。如果找到,直接用输入值(或打开包装的值)和finish
设置变量。
3.在没有找到访问器或实例变量时,调用setValue:forUndefinedKey:
。这在默认情况下会引发一个异常,但是NSObject
的一个子类可能提供特定于键的行为。
举个例子:比如我们要给LPPerson
的name
赋值,首先会去找_name
,如果没有没有找到,就会找_isName
,还是没有找name
,最后再找isName
。如果找到了就直接设值,没有找到就会调用setValue:forUndefinedKey
,来抛出异常。
2.getter
Search Pattern for the Basic Getter
The default implementation of valueForKey:, given a key parameter as input, carries out the following procedure, operating from within the class instance receiving the valueForKey: call.
1.Search the instance for the first accessor method found with a name like get, , is , or _ , in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.
2.If no simple accessor method is found, search the instance for methods whose names match the patterns countOfand objectIn AtIndex: (corresponding to the primitive methods defined by the NSArray class) and AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).
If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.
The proxy object subsequently converts any NSArray messages it receives to some combination of countOf, objectIn AtIndex:, and AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get :range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.
3.If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf, enumeratorOf , and memberOf : (corresponding to the primitive methods defined by the NSSet class).
If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.
This proxy object subsequently converts any NSSet message it receives into some combination of countOf, enumeratorOf , and memberOf : messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.
4.If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _, _is , , or is , in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.
5.If the retrieved property value is an object pointer, simply return the result.
If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.
If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.
6.If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
基本Getter
的搜索模式
valueForKey:
的默认实现,给定一个key
参数作为输入,执行以下过程,从接收valueForKey:
调用的类实例内部进行操作。
1.在实例中搜索找到的第一个访问器方法,其名称如下:get
,,
is
,或_< Key>
。如果找到了,就调用它,并在步骤5中处理结果。否则继续下一步。
2.如果没有找到简单的访问器方法,则在实例中搜索名称与模式countOf
和objectIn
(对应于AtIndex: NSArray
类定义的基本方法)和(对应于
AtIndexes: NSArray
方法objectsAtIndexes:)
匹配的方法。
如果找到第一个和至少两个中的一个,则创建一个集合代理对象,该对象响应所有NSArray
方法并返回该方法。否则,继续执行步骤3。
代理对象随后将接收到的任何NSArray
消息转换为countOf
、objectIn
和AtIndex: 的一些组合,这些组合将消息发送给创建它的符合键值编码的对象。如果原始对象还实现了一个名为
AtIndexes: get
的可选方法,代理对象也会在适当的时候使用它。实际上,代理对象与与键值编码兼容的对象一起工作,允许底层属性像:range: NSArray
一样工作,即使它不是NSArray
。
3.如果没有找到简单的访问方法或数组访问方法组,查找三个方法,分别为countOf
,enumeratorOf
,memberOf
(对应于: NSSet
类定义的原语方法)。
如果这三个方法都找到了,创建一个集合代理对象,它响应所有NSSet
方法并返回那个。否则,继续执行步骤4。
这个代理对象随后将它接收到的任何NSSet消息转换为countOf
、enumeratorOf
和memberOf
消息的组合,并发送给创建它的对象。实际上,代理对象与遵循键值编码的对象一起工作,允许底层属性像: NSSet
一样运行,即使它不是NSSet
。
4.如果没有找到简单的访问方法或集合访问方法组,并且如果接收方的类方法accessinstancevariables
直接返回YES
,那么按照顺序搜索一个实例变量:_
,_is< key>
,,或
is< key>
。如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。
5.如果检索到的属性值是一个对象指针,只需返回结果。
如果值是NSNumber
支持的标量类型,将其存储在NSNumber
实例中并返回。
如果结果是NSNumber
不支持的标量类型,转换为NSValue
对象并返回它。
6.如果所有其他方法都失败,则调用valueForUndefinedKey:
。这在默认情况下会引发一个异常,但是NSObject
的一个子类可能提供特定于键的行为。
和setter
类似,比如我们要取LPPerson
的name
赋值,首先会去找getName
,如果没有没有找到,就会找name
,还是没有找isName
,再找_name
。如果找到了就直接设值,没有找到就会去实例中搜索名称与模式。
接下来我们通过一个demo演示一下:
新建LPPerson
对象:
@interface LPPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
@implementation LPPerson
#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
//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);
}
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
//MARK: - valueForKey 流程分析 - get, , is, or _,
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
@end
我们在LPPerson
中创建了4个成员变量:_isName , name, isName, _name
,是用来测试文档说的当accessinstancevariables
为YES
时,会访问成员变量。
在Viewcontroller
中实现如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"peter" forKey:@"name"];
}
我们运行工程,查看打印结果:
2020-10-31 10:50:55.580747+0800 002-KVC取值&赋值过程[33506:1118311] -[LPPerson setName:] - peter
可以看到执行了setName
方法,我们把setName
注释掉,再次运行:
2020-10-31 10:52:44.333568+0800 002-KVC取值&赋值过程[33532:1120381] -[LPPerson _setName:] - peter
结果是执行了_setName
方法,我们把_setName
也注释掉,再运行:
2020-10-31 10:53:52.679457+0800 002-KVC取值&赋值过程[33548:1121542] -[LPPerson setIsName:] - peter
结果是执行了setIsName
方法,我们把setIsName
也注释掉,再运行:
这次没有执行_setIsName
方法。
按照文档第二步,说如果没有找到简单的访问器,并且accessinstancevariables
为YES
时,回去查找实例变量,接下来我们来再次验证下,在viewDidLoad
中代码修改为如下:
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"peter" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
}
执行,观察运行结果:
2020-10-31 11:01:07.472392+0800 002-KVC取值&赋值过程[33622:1127379] peter-(null)-(null)-(null)
说明是set
的_name
中的值,我们再修改一下代码:
@interface LPPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
// NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
再次运行:
2020-10-31 13:39:29.052301+0800 002-KVC取值&赋值过程[34763:1208784] peter-(null)-(null)
说明是set
的_isName
中的值,我们再修改一下代码:
@interface LPPerson : NSObject{
@public
// NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
// NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
// NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
NSLog(@"%@-%@",person->name,person->isName);
运行,查看结果:
2020-10-31 13:40:58.689786+0800 002-KVC取值&赋值过程[34789:1210795] peter-(null)
说明是set
的name
的值,我们再修改下:
@interface LPPerson : NSObject{
@public
// NSString *_isName;
// NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
// NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
// NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
// NSLog(@"%@-%@",person->name,person->isName);
NSLog(@"%@",person->isName);
再运行:
2020-10-31 13:42:36.330874+0800 002-KVC取值&赋值过程[34813:1212798] peter
可以看到,也是可以的。
这个时候,我们把LPPerson
的accessInstanceVariablesDirectly
置为NO
,再运行下呢:
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}
可以看到,直接奔溃了。
上述的结果就可以验证到文档说的set
过程。
接下来,我们验证下get
的过程:
先将LPPerson
的成员变量恢复:
@interface LPPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
再在viewDidLoad
中添加代码:
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 设值 和 取值的流程
// 2: KVC - 取值的过程
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
运行查看结果:
2020-10-31 13:48:13.369199+0800 002-KVC取值&赋值过程[34884:1217744] 取值:getName
走到了getName
方法,我们注释掉getName
方法,再次运行:
2020-10-31 13:50:46.767696+0800 002-KVC取值&赋值过程[34911:1220210] 取值:name
可以看到执行了name
方法,我们注释掉name
方法,再次运行:
2020-10-31 13:51:21.653834+0800 002-KVC取值&赋值过程[34924:1221160] 取值:isName
执行了isName
方法,我们再把isName
注释掉,再运行:
2020-10-31 13:51:59.197860+0800 002-KVC取值&赋值过程[34937:1222157] 取值:_name
也执行了_name
方法
同样的,我们把_name
注释掉,并且设置accessinstancevariables
返回YES
,再次修改下ViewDidLoad
方法:
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 设值 和 取值的流程
// 2: KVC - 取值的过程
person->_name = @"_name = peter";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
再次运行:
2020-10-31 13:55:49.859847+0800 002-KVC取值&赋值过程[34997:1226759] 取值:_name = peter
再修改成_isName
:
@interface LPPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 设值 和 取值的流程
// 2: KVC - 取值的过程
// person->_name = @"_name = peter";
person->_isName = @"_isName = peter";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
再运行:
2020-10-31 13:57:15.787149+0800 002-KVC取值&赋值过程[35027:1229056] 取值:_isName = peter
在修改为name
:
@interface LPPerson : NSObject{
@public
// NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 设值 和 取值的流程
// 2: KVC - 取值的过程
// person->_name = @"_name = peter";
// person->_isName = @"_isName = peter";
person->name = @"name = peter";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
运行:
2020-10-31 13:58:42.396675+0800 002-KVC取值&赋值过程[35052:1231112] 取值:name = peter
最后,我们再修改为isName
:
@interface LPPerson : NSObject{
@public
// NSString *_isName;
// NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 设值 和 取值的流程
// 2: KVC - 取值的过程
// person->_name = @"_name = peter";
// person->_isName = @"_isName = peter";
// person->name = @"name = peter";
person->isName = @"isName = peter";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
运行:
2020-10-31 13:59:52.374174+0800 002-KVC取值&赋值过程[35077:1232750] 取值:isName = peter
我们再把accessinstancevariables
设置为NO
试试:
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}
同样奔溃了,上述结果都验证了文档的过程。
所以可见文档的重要性,我们一定要要成良好的查阅文档的习惯,可以帮助我们正确的理解。