KVC那点儿事

为了表示我对简书『饱醉豚』事件的不满,简书不再更新,后续有文章只更新 个人博客掘金

欢迎移步 个人博客或者 掘金

本文首发于个人博客

前言

  • 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:的原理

  1. 按照 setKey:_setKey: 的顺序查找方法
  2. 如果找到了方法,就传递参数,调用方法
  3. 如果没有找到,查看 accessInstanceVariablesDirectly 方法的返回值
  4. 如果accessInstanceVariablesDirectly 返回值为 NO 调用
    setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException
  5. 如果accessInstanceVariablesDirectly 返回值为 YES 按照_key_isKeykeyisKey的顺序查找成员变量
  6. 如果找到了成员变量,就直接赋值。
  7. 如果 _key_isKeykeyisKey的顺序没有查找到成员变量就调用setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException

证明赋值

先证明 按照 setKey:_setKey: 的顺序查找方法

YZPerson.hYZPerson.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

  1. 如果accessInstanceVariablesDirectly 返回值为 NO 调用setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException
  2. 如果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_isKeykeyisKey的顺序查找成员变量

代码还是上面的代码,打断点,然后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内部调用了 willChangeValueForKeydidChangeValueForKey

取值 valueForKey:的原理

  1. 按照getKeykeyisKey_key的顺序查找方法
  2. 如果找到了,就直接调用
  3. 如果没找到,就查看accessInstanceVariablesDirectly 方法的返回值
  4. 如果accessInstanceVariablesDirectly 返回值为 NO 调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
  5. 如果accessInstanceVariablesDirectly 返回值为 YES 按照_key_isKeykeyisKey的顺序查找成员变量
  6. 如果找到了成员变量,就直接取值。
  7. 如果 _key_isKeykeyisKey的顺序没有查找到成员变量就调用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。

上面验证了 按照getKeykeyisKey_key的顺序查找方法

其他的验证逻辑,和赋值验证过程一致,就不赘述了。

本文相关代码github地址 github

本文参考资料:

Runtime源码

iOS底层原理

更多资料,欢迎关注个人公众号,不定时分享各种技术文章。

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • KVC是Key Value Coding的简称。它是一种可以通过字符串的名字(key)来访问类属性的机制。而不是通...
    _李恒阅读 736评论 0 0
  • KVC(Key-valuecoding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS...
    榕樹頭阅读 693评论 0 2
  • 【原创博文,转载请注明出处!】之前做iOS开发的时候经常使用KVO来监听对象属性值的变化去执行一些操作,但是从未思...
    RephontilZhou阅读 1,090评论 1 9
  • KVC简单介绍 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key...
    公子无礼阅读 1,376评论 0 6
  • 1.KVC 关于 KVC 和 KVO ,我之前的总结文章有写过,但是趋于表面,没有探究其内部真正的实现原理和进阶用...
    Liberalism阅读 1,074评论 0 5