KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问一个属性。
常见的API有
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
我们先来了解一下KVC的基本使用。
@interface Cat : NSObject
@property (nonatomic, assign) int weight;
@end
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, strong) Cat *cat;
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
Cat *cat = [[Cat alloc] init];
person.cat = cat;
//设值
//以下三句都可以为age设值
person.age = 10;
[person setValue:@11 forKey:@"age"];
[person setValue:@12 forKeyPath:@"age"];
//以下两句都可以为weight设值
cat.weight = 5;
[person setValue:@6 forKeyPath:@"cat.weight"];
//取值
NSLog(@"%d - %@ - %@",person.age,[person valueForKey:@"age"],[person valueForKeyPath:@"age"]);
NSLog(@"%d - %@",person.cat.weight,[person valueForKeyPath:@"cat.weight"]);
}
@end
打印结果
2019-06-28 09:52:53.175189+0800 KVC底层原理(设值取值原理)[7779:29480939] 12 - 12 - 12
2019-06-28 09:52:53.175300+0800 KVC底层原理(设值取值原理)[7779:29480939] 6 - 6
由上面代码可以看出,setValue: forKey:、setValue: forKeyPath:都可以设值,valueForKey:、valueForKeyPath:都可以取值。keyPath结尾的两个方法,可以从路径上设置和取值,稍微更强大一点。
KVC设值原理
首先我们来看一张KVC设值流程原理图,setValue:forKey:内部调用流程
- 1.按照setKey:,_setKey:顺序查找方法,找到了就调用方法传递参数。
- 2.第一步没找到就会调用accessInstanceVariablesDirectly方法,该方法返回值为NO时直接调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException,方法返回值是YES的时候进入第三步。该方法默认值是返回YES。
- 3.按照_key、_isKey、key、isKey顺序查找成员变量,找到了就直接赋值,没找到依然是调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException。
接下来我们用代码验证一下以上流程
- 首先在Person类.h中不定义属性,只在.m文件实现setAge:和_setAge:方法,此时在ViewController类中创建Person的实例对象person,通过KVC给age赋值。
运行程序,会优先调用setAge:方法。
@implementation Person
- (void)setAge:(int)age{
NSLog(@"setAge: - %d",age);
}
- (void)_setAge:(int)age{
NSLog(@"_setAge: - %d",age);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person setValue:@10 forKey:@"age"];
}
@end
打印结果
2019-06-28 11:20:56.582913+0800 KVC底层原理(设值取值原理)[8766:29719538] setAge: - 10
注释setAge:方法则会调用_setAge:方法
@implementation Person
//- (void)setAge:(int)age{
// NSLog(@"setAge: - %d",age);
//}
- (void)_setAge:(int)age{
NSLog(@"_setAge: - %d",age);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person setValue:@10 forKey:@"age"];
}
@end
运行结果
2019-06-28 11:21:56.416325+0800 KVC底层原理(设值取值原理)[8793:29725184] _setAge: - 10
- 此时我们在Person.h中直接定义成员变量,因为定义属性,系统会自动生成set方法,拿样就验证不了下面的步骤。所以分别定义_age,_isAge,age,isAge四个成员变量,并且实现accessInstanceVariablesDirectly方法,分别研究在返回值为YES和NO的流程。
1>返回值为NO,直接崩溃报错NSUnknownKeyException
@interface Person : NSObject
{
int _age;
int _isAge;
int age;
int isAge;
}
@end
//- (void)setAge:(int)age{
// NSLog(@"setAge: - %d",age);
//}
//- (void)_setAge:(int)age{
// NSLog(@"_setAge: - %d",age);
//}
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person setValue:@10 forKey:@"age"];
}
@end
运行结果
2019-06-28 11:43:00.233200+0800 KVC底层原理(设值取值原理)[9049:29783240]
*** Terminating app due to uncaught exception 'NSUnknownKeyException',
reason: '[ setValue:forUndefinedKey:]:
this class is not key value coding-compliant for the key age.'
2>返回值为YES,首先会_age成员变量赋值
接着会给_isAge赋值
再接着会给age赋值
最后会给isAge赋值
如果连四个成员变量都找不到,还是会崩溃然后报错NSUnknownKeyException
KVC取值原理
我们再来看KVC的取值原理流程图,valueForKey:内部调用流程
- 1.按照getKey、key、isKey、_isKey顺序查找方法,找到方法了直接调用方法。
- 2.第一步没找到就会调用accessInstanceVariablesDirectly方法,该方法返回值为NO时直接调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException,方法返回值是YES的时候进入第三步。该方法默认值是返回YES。
-
3.按照_key、_isKey、key、isKey顺序查找成员变量,找到了就直接取值,没找到依然是调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException。
接下来我们同样验证一下以上流程
- 首先在Person类.h中不定义属性,只在.m文件实现getAge、age、isAge、_age方法,此时在ViewController类中创建Person的实例对象person,通过KVC取age值。
运行程序,首先调用getAge方法
@implementation Person
- (int)getAge{
return 10;
}
- (int)age{
return 11;
}
- (int)isAge{
return 12;
}
- (int)_age{
return 13;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%@",[person valueForKey:@"age"]);
}
@end
打印结果
2019-06-28 14:39:33.612821+0800 KVC底层原理(设值取值原理)[10887:30101949] 10
然后依次注释getAge方法,运行打印结果为11,代表接下来会执行age方法;
再注释age方法,运行打印结果为12,代表接下来会执行isAge方法;
再注释isAge方法,运行打印结果是13,代表接下来执行的是_age方法。
- 接下来同样在Person.h中定义age,_isAge,age,isAge四个成员变量,并且实现accessInstanceVariablesDirectly方法,分别研究在返回值为YES和NO的流程。
1>返回值为NO,直接崩溃报错NSUnknownKeyException
@interface Person : NSObject
{
int _age;
int _isAge;
int age;
int isAge;
}
@end
@implementation Person
//- (int)getAge{
// return 10;
//}
//
//- (int)age{
// return 11;
//}
//
//- (int)isAge{
// return 12;
//}
//
//- (int)_age{
// return 13;
//}
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%@",[person valueForKey:@"age"]);
}
@end
2>返回值为YES,首先会取_age成员变量的值
@interface Person : NSObject
{
@public
int _age;
int _isAge;
int age;
int isAge;
}
@end
@implementation Person
//- (int)getAge{
// return 10;
//}
//
//- (int)age{
// return 11;
//}
//
//- (int)isAge{
// return 12;
//}
//
//- (int)_age{
// return 13;
//}
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
person->_age = 10;
person->_isAge = 11;
person->age = 12;
person->isAge = 13;
NSLog(@"%@",[person valueForKey:@"age"]);
}
@end
打印结果
2019-06-28 14:56:30.994833+0800 KVC底层原理(设值取值原理)[11104:30169895] 10
然后依次注释_age属性以及赋值代码,运行打印结果为11,代表接下来会取_isAge的值;
再注释_isAge属性以及赋值代码,运行打印结果为12,代表接下来会取age的值;
再注释age属性以及赋值代码,运行打印结果为13,代表接下来会取isAge的值;
最后连isAge属性也注释掉,运行直接崩溃报错NSUnknownKeyException
面试题
1.通过KVC修改属性,会触发KVO吗?
答:是会触发KVO的。
以下通过代码验证一下
如果以下验证有不明白的地方,可以结合上篇文章KVO底层原理
1>通过KVC改变属性的值,答案是可以触发KVO的。因为定义属性,系统会自动生成set方法,而调用了set方法,自然就会触发KVO。
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[person setValue:@10 forKey:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到属性值%@的变化 - %@ - %@",keyPath,object,change);
}
打印结果
2019-06-28 15:37:48.288540+0800 KVC修改属性是否触发KVO[11731:30311788] 监听到属性值age的变化 - - {
kind = 1;
new = 10;
old = 0;
}
2>通过KVC直接改变成员变量的值,答案也是可以触发KVO。
@interface Person : NSObject{
@public
int _age;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person addObserver:self forKeyPath:@"_age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[person setValue:@10 forKey:@"_age"];
[person removeObserver:self forKeyPath:@"_age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到属性值%@的变化 - %@ - %@",keyPath,object,change);
}
@end
打印结果
2019-06-28 15:50:11.932356+0800 KVC修改属性是否触发KVO[11881:30355219] 监听到属性值_age的变化 - - {
kind = 1;
new = 10;
old = 0;
}
上一篇文章KVO的底层原理我们说过,直接修改成员变量的值,因为不会调用set方法,所以是不会调用KVO的。为什么现在使用KVC修改成员变量的值就可以触发KVO呢?我推测是因为KVC内部调用了willChangeValueForKey:和didChangeValueForKey:方法手动触发了KVO。
接下来我们在Person的.m文件中来重写这两个方法,验证一下是否内部真的有调用,如果有调用,则我们的猜想成立。
@interface Person : NSObject{
@public
int _age;
}
@end
@implementation Person
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey - %@",key);
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin - %@",key);
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end - %@",key);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person addObserver:self forKeyPath:@"_age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[person setValue:@10 forKey:@"_age"];
[person removeObserver:self forKeyPath:@"_age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到属性值%@的变化 - %@ - %@",keyPath,object,change);
}
@end
打印结果
2019-06-28 16:00:41.718526+0800 KVC修改属性是否触发KVO[12005:30388256] willChangeValueForKey - _age
2019-06-28 16:00:41.718719+0800 KVC修改属性是否触发KVO[12005:30388256] didChangeValueForKey - begin - _age
2019-06-28 16:00:41.718855+0800 KVC修改属性是否触发KVO[12005:30388256] 监听到属性值_age的变化 - - {
kind = 1;
new = 10;
old = 0;
}
2019-06-28 16:00:41.718943+0800 KVC修改属性是否触发KVO[12005:30388256] didChangeValueForKey - end - _age