KVC
KVC: 全称Key-Value Coding,也称为键值编码。
KVC可以通过一个key间接访问某个对象属性。
KVC有两个特性:
- 可以访问私有成员变量;
- 可以修改私有或者系统的成员属性;
KVC的定义都是对NSObject的扩展来实现的(Objective-C中有个显式的NSKeyValueCoding类别名)。所以对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用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"];//间接赋值
[person1 setValue:@"Red" forKeyPath:@"car.color"];//属性里面的属性
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;
即,使用成员变量name,而不是属性name
设置KVC的属性值改变,也可以使用KVO监听到属性值的改变,即触发KVO
因此,可以说,KVC是基于KVO的实现,KVC修改属性是可以触发KVO的。
KVC的工作流程
setValue:forKey:大致是这样工作的:
主要是:
先找方法
方法找到了,直接执行;
方法没有找到,则
判断是否可以直接访问属性
如果不可以直接访问属性,则Crash
如果可以执行访问属性,则
判断有没有响应的属性值
如果找到了属性,则直接执行;
如果没有找到属性值,则Crash
valueForKey:大致是这样工作的:
更多学习关于KVC:
iOS开发技巧系列—详解KVC(我告诉你KVC的一切)
KVC修改:字典、数组、对象属性、结构体
结构体的话,前面应该加上&,将地址传进去
KVC的Crash相关
问:哪些可能会造成KVC的Crash?
设置阶段的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、2种类型错误
1. key 不是对象的属性值
2. keyPath 不正确
造成的Crash
3. key 为nil
造成的Crash
我们可以利用 Method Swizzling 方法,在 NSObject 的分类中将 setValue:forKey:
和 ysc_setValue:forKey:
进行方法交换。然后在自定义的方法中,添加对 key 为 nil 这种类型的判断。
4. value 为 nil
为非对象设值,造成崩溃 的情况
在调用 setValue:forKey:
方法时,系统如果查找到名为 set<Key>:
方法的时候,会去检测 value 的参数类型,如果参数类型为 NSNmber 的标量类型或者是 NSValue 的结构类型,但是 value 为 nil 时,会自动调用 setNilValueForKey:
方法。
这个方法的默认实现会引发崩溃。
所以为了防止这种情况导致的崩溃,我们可以通过重写 setNilValueForKey:
来解决。
- (void)setNilValueForKey:(NSString *)key
{
NSLog(@"不能将%@设成nil",key);
}
取值阶段的Crash
- 取值的时候,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"]);
,该方法编译器不通过
取值阶段的Crash可通过:
1和2可以通过重写valueForUndefinedKey:
方法
3通过方法交换,重写valueForUndefinedKey:
,在方法里面加非空判断
4重写setValue:forKey:
方法
避免Crash
面试题
- KVC能否改变对象的值
跟KVO监听对象一样,又是一个类似的题,经过试验发现,可以。
#import "YZPerson.h"
@interface ViewController ()
@property (strong, nonatomic) YZPerson *person;
@end
@implementation ViewController
//MRC下可以调用这个
//- (void)setPerson:(YZPerson *)person
//{
// if (_person != person) {
// [_person release];
// _person = [person retain];
// }
//}
- (void)viewDidLoad {
[super viewDidLoad];
YZPerson *person = [[YZPerson alloc] init];
self.person = person;
person.name = @"person";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSObject *obj = [[NSObject alloc] init];
[self setValue:obj forKeyPath:@"person"];//间接赋值
NSLog(@"%@", self.person);
}
打印结果:
<NSObject: 0x6000028ae940>
可以看出,通过KVC可以将对象进行改变。
被问到的面试题:
下面两个方法的区别:
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;
setValue:forkey是KVC里面的方法
可以修改:
字典、数组、对象属性、结构体
setObject:forKey是NSMutableDictionary里面的方法,是为字典赋值
那么,在都是对NSMutableDictionary进行修改的情况下,
NSMutableDictionary * dic = [@{@"name":@"小明"} mutableCopy];
//都可以进行非空赋值
[dic setValue:@"Men" forKey:@"gender"];
[dic setObject:@"Men2" forKey:@"gender"];
//setValue可以赋值nil,相当于删除key = gender的键值对
[dic setValue:nil forKey:@"gender"];
//setObject赋值nil,会崩溃
//[dic setObject:nil forKey:@"gender"];