kvc本质
KVC
KVC: 全称Key-Value Coding,也称为键值编码。
KVC可以通过一个key间接访问某个对象属性。
KVC有两个特性:
- 可以访问私有成员变量;
- 可以修改私有或者系统的成员属性;
KVC有以下四种方法:
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
其中,前两个是设置值方法,后面两个是取值方法。
KeyPath可以使用子类里面的数据。Key不可以。
我们简单看一下KVC的使用:
YZPerson *person1 = [[YZPerson alloc] init];
person1.age = 10;//直接赋值
[person1 setValue:@"jack" forKey:@"name"];//间接赋值
[person1 setValue:@"120" forKeyPath:@"weight"];//间接赋值
NSLog(@"person1.age = %@, person1.name = %@, person1.weight = %d", [person1 valueForKey:@"age"], [person1 valueForKeyPath:@"name"], person1.weight);
运行结果:
2020-02-28 11:51:53.654921+0800 Category[1399:80227] person1.age = 10, person1.name = jack, person1.weight = 120
问:KVC修改属性是否可以触发KVO?
找个例子我们试一下:
@interface ViewController ()
@property (strong, nonatomic) Persion *p1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Persion *p1 = [[Persion alloc] init];
self.p1 = p1;
[self.p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"context"];
[self.p1 setValue:@"rose" forKey:@"name"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@ %@ %@", object, change, context);
}
- (void)dealloc
{
[self.p1 removeObserver:self forKeyPath:@"name"];
}
@end
运行结果:
2020-02-28 13:48:56.604542+0800 test001[1432:397700] <Persion: 0x283918fa0> {
kind = 1;
new = rose;
old = "<null>";
} context
从结果可以看出,使用KVC修改属性值(name),可以触发KVO的监听。
且,经过验证,[self.p1 setValue:@"rose" forKey:@"name"];
这句代码进入了
(void)setName:(NSString *)name
方法里面。
也就是KVC的改变属性值,进入了属性的setter方法里面,
从而在didChangeValueForKey:
方法中发送通知,实现KVO的监听。
并且,即使不写- (void)setName:(NSString *)name
方法
或者不写@property (strong, nonatomic) NSString *name;
设置KVC的属性值改变,也可以使用KVO监听到属性值的改变,即触发KVO
因此,可以说,KVC是基于KVO的实现,KVC修改属性是可以触发KVO的。
KVC的工作流程
setValue:forKey:大致是这样工作的:
主要是:
先找方法
方法找到了,直接执行;
方法没有找到,则
判断是否可以直接访问属性
如果不可以直接访问属性,则Crash
如果可以执行访问属性,则
判断有没有响应的属性值
如果找到了属性,则直接执行;
如果没有找到属性值,则Crash
valueForKey:大致是这样工作的:
KVC的Crash相关
问:哪些可能会造成KVC的Crash?
设置
设置值的时候,Key找不到,Key为nil,value为nil,value类型不匹配就会Crash
以下几种方法,都会造成KVC的Crash:
key 不是对象的属性值,造成崩溃[self.person setValue:@"10" forKey:@"age2"];
keyPath 不正确,造成崩溃[self.person setValue:@"10" forKeyPath:@"age.xxx"];
key 为 nil,造成崩溃[self.person setValue:@"10" forKey:nil];
value 为 nil,造成崩溃[self.person setValue:nil forKey:@"age"];
value类型不对[self.person setValue:[[NSObject alloc] init] forKey:@"age"];
根据KVC设置时的查找过程,我们发现,当setValue:forKey:
执行失败会调用 setValue: forUndefinedKey:
方法,并引发崩溃。
那么,我们可以通过重写setValue: forUndefinedKey:
来避免
1. key 不是对象的属性值
2. keyPath 不正确
造成的Crash
造成的Crash
我们可以利用 Method Swizzling 方法,在 NSObject 的分类中将 setValue:forKey:
和ysc_setValue:forKey:
进行方法交换。然后在自定义的方法中,添加对 key 为 nil 这种类型的判断。
4. value 为 nil
为非对象设值,造成崩溃 的情况
在调用 setValue:forKey:方法时,系统如果查找到名为 set
这个方法的默认实现会引发崩溃。
所以为了防止这种情况导致的崩溃,我们可以通过重写 setNilValueForKey:来解决。
- (void)setNilValueForKey:(NSString *)key
{
NSLog(@"不能将%@设成nil",key);
}
取值
取值的时候,Key找不到
当取值的时候,如果找不到key或者key为nil,也会Crash
key 不是对象的属性值,造成崩溃NSLog(@"%@", [self.person valueForKey:@"age2"]);
keyPath 不正确,造成崩溃NSLog(@"%@", [self.person valueForKeyPath:@"age.xxx"]);
key 为 nil,造成崩溃NSLog(@"%@", [self.person valueForKey:nil]);
value 为 nil,NSLog(@"%@", [nil valueForKey:@"age"]);,该方法编译器不通过
1和2可以通过重写valueForUndefinedKey:
方法
3通过方法交换,重写valueForUndefinedKey:
,在方法里面加非空判断
4重写setValue:forKey:
方法