KVC底层原理(设置取值原理)

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。
KVC设值原理图

接下来我们用代码验证一下以上流程

  1. 首先在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
  1. 此时我们在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: '[<Person 0x6000002a9f00> setValue:forUndefinedKey:]: 
this class is not key value coding-compliant for the key age.'

2>返回值为YES,首先会_age成员变量赋值


_age赋值

接着会给_isAge赋值


_isAge赋值

再接着会给age赋值
age赋值

最后会给isAge赋值


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。


    KVC取值原理图.png

接下来我们同样验证一下以上流程

  1. 首先在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方法。

  1. 接下来同样在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<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到属性值%@的变化 - %@ - %@",keyPath,object,change);
}

打印结果
2019-06-28 15:37:48.288540+0800 KVC修改属性是否触发KVO[11731:30311788] 监听到属性值age的变化 - <Person: 0x6000018f08c0> - {
    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<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到属性值%@的变化 - %@ - %@",keyPath,object,change);
}

@end

打印结果
2019-06-28 15:50:11.932356+0800 KVC修改属性是否触发KVO[11881:30355219] 监听到属性值_age的变化 - <Person: 0x600002ee8850> - {
    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<NSKeyValueChangeKey,id> *)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的变化 - <Person: 0x600001cdc7a0> - {
    kind = 1;
    new = 10;
    old = 0;
}
2019-06-28 16:00:41.718943+0800 KVC修改属性是否触发KVO[12005:30388256] didChangeValueForKey - end - _age
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • 【原创博文,转载请注明出处!】之前做iOS开发的时候经常使用KVO来监听对象属性值的变化去执行一些操作,但是从未思...
    RephontilZhou阅读 1,085评论 1 9
  • 前言:往往会某项工具WORK,就想究其原理。本文先简单介绍KVC 一、KVC 简介 1.1 KVC 概述 1.KV...
    梦蕊dream阅读 893评论 0 2
  • 设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的...
    卑微的戏子阅读 618评论 0 1
  • # The Ruby Style Guide > Hey jude, don't make it bad. > T...
    司徒雷斯阅读 309评论 0 2
  • 读经日记第72篇 2018年10月31号 星期三 天气:晴, 系统读经30周3天 共213天 读经方法:13...
    群策群辉阅读 275评论 0 0