KVC

241 阅读4分钟

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