前言
- KVC是Key Value Coding的简称。它是一种可以通过字符串的名字(key)来访问类属性的机制。而不是通过调用Setter、Getter方法访问。KVC的方法定义在Foundation/NSKeyValueCoding中。
- KVC和KVO都属于键值编程而且底层实现机制都是isa-swizzing。
常见的API有
- -(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- -(void)setValue:(id)value forKey:(NSString *)key;
- -(id)valueForKeyPath:(NSString *)keyPath;
- -(id)valueForKey:(NSString *)key;
KVC基本使用
- 定义一个YZPerson类,有个 name 属性
@interface YZPerson : NSObject
@property (nonatomic,strong) NSString *name;
@end复制代码- ViewController 控制器中,如下使用
#import "ViewController.h"
#import "YZPerson.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
YZPerson *person = [[YZPerson alloc]init];
// 赋值
[person setValue:@"jack" forKey:@"name"];
// 取值
NSLog(@"%@",[person valueForKey:@"name"]);
}
@end复制代码- 结果为
KVCDemo[25838:347883] jack复制代码赋值 setValue:forKey:的原理
- 按照 setKey:、_setKey: 的顺序查找方法
- 如果找到了方法,就传递参数,调用方法
- 如果没有找到,查看 accessInstanceVariablesDirectly 方法的返回值
- 如果accessInstanceVariablesDirectly 返回值为 NO 调用 setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException
- 如果accessInstanceVariablesDirectly 返回值为 YES 按照_key、_isKey、key、isKey的顺序查找成员变量
- 如果找到了成员变量,就直接赋值。
- 如果 _key、_isKey、key、isKey的顺序没有查找到成员变量就调用setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException
证明赋值
先证明 按照 setKey:、_setKey: 的顺序查找方法
YZPerson.h 和 YZPerson.m 如下
// 只有name属性,没有age
@interface YZPerson : NSObject
@property (nonatomic,strong) NSString *name;
@end
@implementation YZPerson
- (void)setAge:(int)age
{
NSLog(@"setAge: - %d", age);
}
- (void)_setAge:(int)age
{
NSLog(@"_setAge: - %d", age);
}
复制代码调用地方
YZPerson *person = [[YZPerson alloc]init];
// 赋值
[person setValue:@20 forKey:@"age"];
复制代码打印结果是
KVCDemo[26389:357519] setAge: - 20复制代码说明调用来的是setAge: 那如果 去掉 setAge: 呢
// 只有name属性,没有age
@interface YZPerson : NSObject
@property (nonatomic,strong) NSString *name;
@end
@implementation YZPerson
- (void)setAge:(int)age
{
NSLog(@"setAge: - %d", age);
}
- (void)_setAge:(int)age
{
NSLog(@"_setAge: - %d", age);
}
复制代码结果为:
KVCDemo[26594:360894] _setAge: - 20复制代码证明了 按照 setKey:、_setKey: 的顺序查找方法
证明 accessInstanceVariablesDirectly
- 如果accessInstanceVariablesDirectly 返回值为 NO 调用setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException
- 如果accessInstanceVariablesDirectly 返回值为 YES 就去查找成员变量,就直接赋值。
- 我们在 YZPerson.h 中定义四个成员变量, YZPerson.m中 只有accessInstanceVariablesDirectly 并返回NO
@interface YZPerson : NSObject
{
@public
int age;
int isAge;
int _isAge;
int _age;
}
@property (nonatomic,strong) NSString *name;
@end
#import "YZPerson.h"
@implementation YZPerson
// 默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return NO;
}
@end
复制代码运行报错:找不到 key值 age
KVCDemo[27163:369895] *** Terminating app due to uncaught exception
'NSUnknownKeyException', reason: '[<YZPerson 0x600003a5e700>
setValue:forUndefinedKey:]: this class is not key value
coding-compliant for the key age.'复制代码- 我们把YZPerson.m中 只有accessInstanceVariablesDirectly 返回YES
运行结果:
KVCDemo[27385:373752] 20复制代码证明是按照_key、_isKey、key、isKey的顺序查找成员变量
代码还是上面的代码,打断点,然后LLDB调试
(lldb) po person->_age
20
(lldb) po person->_isAge
<nil>
(lldb) po person->age
<nil>
(lldb) po person->isAge
<nil>
复制代码如果去掉成员变量_age
结果为
(lldb) po person->_isAge
20
(lldb) po person->age
<nil>
(lldb) po person->isAge
<nil>
复制代码同理其他的几种情况,读者可自行尝试 demo。
KVC与KVO
通过关于KVO看这篇就够了 我们知道
KVO的本质
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey:
- 父类原来的setter
- didChangeValueForKey:
- 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
那么KVC能否触发KVO呢,
我们在 YZPerson.h中书写如下代码
@interface YZPerson : NSObject
{
@public
int age;
int isAge;
int _isAge;
int _age;
}
@property (nonatomic,strong) NSString *name;
@end复制代码我们知道,成员变量是不会生成set 和 get方法的 然后 YZPerson.m中书写如下代码
#import "YZPerson.h"
@implementation YZPerson
// 默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return YES;
}
@end
复制代码在VC中设置KVO监听
#import "ViewController.h"
#import "YZPerson.h"
@interface ViewController ()
@property (nonatomic,strong) YZPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[YZPerson alloc]init];
// 添加KVO监听
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
// 通过KVC修改age属性
[self.person setValue:@10 forKey:@"age"];
// 取值
NSLog(@"取值为:%@",[self.person valueForKey:@"age"]);
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"observeValueForKeyPath - %@", change);
}
-(void)dealloc{
// 移除KVO监听
[self.person removeObserver:self forKeyPath:@"age"];
}
复制代码输出结果为:
KVCDemo[28271:388786] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
KVCDemo[28271:388786] 取值为:10复制代码可知,其实在系统内部,是调用了
- willChangeValueForKey:
- didChangeValueForKey:复制代码进一步验证
然后 YZPerson.m中书写如下代码
#import "YZPerson.h"
@implementation YZPerson
- (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);
}
// 默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return YES;
}
@end
复制代码输出结果为:
KVCDemo[28392:390730] willChangeValueForKey - age
KVCDemo[28392:390730] didChangeValueForKey - begin - age
KVCDemo[28392:390730] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
KVCDemo[28392:390730] didChangeValueForKey - end - age
KVCDemo[28392:390730] 取值为:10复制代码所以,足以说明,KVC内部调用了 willChangeValueForKey 和 didChangeValueForKey
GNU验证
文章发出去之后,YiHuaXie 评论说
我觉得你的那个KVC能触发KVO的理由说的比较牵强,willChangeValueForKey 和 didChangeValueForKey的调用只能证明触发KVO的时候会调用这两个方法,并不能证明KVC一定也能调用了,只是说从结果上看上去是那么回事
因为苹果官方对 setValue: forKey:的源码是不开源的,验证的时候,是从结果来反推回去的,因为调用了KVC之后,确实会打印willChangeValueForKey和didChangeValueForKey,算是间接验证吧。
之后我又查了GNUstep的源码
简单说下GNUstep 这是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然不是官方源码,但是还是有一定的参考价值的
GNUstep,GNU计划的项目之一。它将Cocoa(前身为NeXT的OpenStep)Objective-C软件库,部件工具箱(widget toolkits)以及其上的应用软件,以自由软件方式重新实现。它能够运行在类Unix操作系统上,也能运作在Microsoft Windows上。 ---- 维基百科
GNUstep的官方网址为GNUstep
下载gnustep-base-1.26.0工程,在 NSKeyValueObserving.m中有如下代码
- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
Class c = [self class];
void (*imp)(id,SEL,id,id);
imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];
if ([[self class] automaticallyNotifiesObserversForKey: aKey])
{
[self willChangeValueForKey: aKey];
imp(self,_cmd,anObject,aKey);
[self didChangeValueForKey: aKey];
}
else
{
imp(self,_cmd,anObject,aKey);
}
}复制代码可以作为setValue: forKey会调用willChangeValueForKey和didChangeValueForKey的佐证吧。
取值 valueForKey:的原理
- 按照getKey、key、isKey、_key的顺序查找方法
- 如果找到了,就直接调用
- 如果没找到,就查看accessInstanceVariablesDirectly 方法的返回值
- 如果accessInstanceVariablesDirectly 返回值为 NO 调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
- 如果accessInstanceVariablesDirectly 返回值为 YES 按照_key、_isKey、key、isKey的顺序查找成员变量
- 如果找到了成员变量,就直接取值。
- 如果 _key、_isKey、key、isKey的顺序没有查找到成员变量就调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
验证取值
- 取值和赋值的大体逻辑基本一致
然后 YZPerson.m中书写如下代码
#import "YZPerson.h"
@implementation YZPerson
- (int)getAge
{
return 11;
}
- (int)age
{
return 12;
}
- (int)isAge
{
return 13;
}
- (int)_age
{
return 14;
}
// 默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return YES;
}
@end
复制代码VC中如下代码
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[YZPerson alloc]init];
// 取值
NSLog(@"取值为:%@",[self.person valueForKey:@"age"]);
}
复制代码结果为
KVCDemo[29145:403008] 取值为:11复制代码如果去掉
- (int)getAge
{
return 11;
}复制代码则,输出结果为12。
上面验证了 按照getKey、key、isKey、_key的顺序查找方法
其他的验证逻辑,和赋值验证过程一致,就不赘述了。
本文相关代码github地址 github
本文参考资料:
来源:本文为第三方转载,如有侵权请联系小编删除。