KVC 概述
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性或者通过一个 Key 给某个属性赋值。
常见API:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
其中方法 forKeyPath: 和 forKey: 均可用于对对象某个属性(keyPath 或 key 的值即为@"属性")赋值。二者的区别是,forKeyPath 用于对象B是对象A的一个属性,B 中有propertyX,此时可直接通过对象 A 设置 B 中 propertyX。
如下图所示:Person 中存在 wangCai(Dog类实例对象)对象属性,weight 为 Dog 的一个属性。此时可通过如下 forKeyPath 方式直接设置 wangCai 的 weight 属性值:
测试代码:
Person *p = [Person new];
[p setValue:@(18) forKey:@"age"];
NSLog(@"p.age(forKey):%d", p.age);
[p setValue:@(28) forKeyPath:@"age"];
NSLog(@"p.age(forKeyPath):%d", p.age);
Dog *d = [Dog new];
p.wangCai = d;
[p setValue:@(22) forKeyPath:@"wangCai.weight"];
NSLog(@"p.wangCai.weight(forKeyPath):%d", p.wangCai.weight);
日志输出:
KVCTest[47592:706992] p.age(forKey):18
KVCTest[47592:706992] p.age(forKeyPath):28
KVCTest[47592:706992] p.wangCai.weight(forKeyPath):22
KVC 设值原理
新建 Person,不添加 age 属性,然后执行 setValue,key 为 @"age",其过程如下:
优先调用 setAge: 当找不到 setAge: 方法时,去查找 _setAge: 方法
并在.m文件分别中创建方法 - (void)setAge:(int)age 和 - (void)_setAge:(int)age
Person *p = [Person new];
[p setValue:@(18) forKey:@"age"];
- (void)setAge:(int)age
{
NSLog(@"KVC-先查找 setAge:");
}
- (void)_setAge:(int)age
{
NSLog(@"KVC-再查找 _setAge:");
}
当同时存在上述两个方法时,控制器输出:
KVCTest[82165:1606080] KVC-先查找 setAge:
注释(删除)- (void)setAge:(int)age 方法,再次运行,控制台输出:
KVCTest[82200:1607189] KVC-再查找 _setAge:
删除上述两个方法(setAge: 和 _setAge:),Person 类中添加成员变量
@interface Person : NSObject
{
@public
int _age;
int _isAge;
int age;
int isAge;
}
@end
Person.m
/// 是否允许直接访问成员变量
+ (BOOL)accessInstanceVariablesDirectly
{
return NO;
}
/***********************/
方法返回 NO (不允许直接访问成员变量,抛出异常)
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Person 0x6000025319c0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key age.'
/***********************/
方法返回YES
则按照 _age -> _isAge -> age -> isAge 顺序查找赋值
分别注释相关属性,可一一验证。
KVC 修改属性值,会触发 KVO 吗?
@interface Person : NSObject
{
@public
int _age;
}
Person.m文件中不添加任何和age相关的set方法
Person *p = [Person new];
[p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
// 通过 KVC 修改 age 属性值
[p setValue:@(18) forKey:@"age"];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"KVC 触发 KVO - change: %@", change);
}
KVCTest[82580:1619751] KVC 触发 KVO - change: {
kind = 1;
new = 18;
old = 0;
}
可以猜测 KVC 内部可能触发了 willChangeValueForKey: 和 didChangeValueForKey: 方法。 即:
/**
[p willChangeValueForKey:@"age"];
p -> _age = 18;
[p didChangeValueForKey:@"age"];
*/
代码大致验证
Person.m
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"KVC 修改成员变量值调用 willChangeValueForKey: %@", key);
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"KVC 修改成员变量值调用 didChangeValueForKey: %@ - 开始", key);
[super didChangeValueForKey:key];
NSLog(@"KVC 修改成员变量值调用 didChangeValueForKey: %@ - 结束", key);
}
log输出:
KVCTest[82886:1629522] KVC 修改成员变量值调用 willChangeValueForKey: age
2020-05-13 23:14:48.697818+0800 KVCTest[82886:1629522] KVC 修改成员变量值调用 didChangeValueForKey: age - 开始
2020-05-13 23:14:48.698080+0800 KVCTest[82886:1629522] KVC 触发 KVO - change: {
kind = 1;
new = 18;
old = 0;
}
2020-05-13 23:14:48.698233+0800 KVCTest[82886:1629522] KVC 修改成员变量值调用 didChangeValueForKey: age - 结束
KVC 取值原理
取值方法查找顺序 getKey -> key -> isKey -> _key
Person.m
- (int)getAge
{
NSLog(@"KVC 取值方法:getAge");
return 8;
}
- (int)key
{
NSLog(@"KVC 取值方法:key");
return 18;
}
- (int)_isKey
{
NSLog(@"KVC 取值方法:_isKey");
return 28;
}
- (int)_key
{
NSLog(@"KVC 取值方法:_key");
return 38;
}
触发:
Person *p = [Person new];
NSLog(@"KVC 取值:%@", [p valueForKey:@"age"]);
控制台log:
KVCTest[83152:1637367] KVC 取值方法:getAge
KVCTest[83152:1637367] KVC 取值:8
当上述方法均找不到时,执行取值逻辑:
取值成员变量查找顺序:_key -> _isKey -> key -> isKey
Person.h
{
@public
int age;
int isAge;
int _age;
int _isAge;
}
触发:
Person *p = [Person new];
p -> _age = 80;
p -> _isAge = 81;
p -> age = 82;
p -> isAge = 83;
NSLog(@"KVC 取值:%@", [p valueForKey:@"age"]);
log 输出:
KVCTest[83320:1643140] KVC 成员变量取值:80