KVO和KVC的使用及原理解析

一 KVO基本使用
二 KVO本质原理讲解及代码验证
三 KVC基本使用
四 KVC设值原理
五 KVC取值原理

KVC: 即 Key-Value-Coding,用于键值编码。作为 cocoa 的一个标准化组成部分,它是基于 NSKeyValueCoding 非正式协议的机制。简单来说,就是直接通过 key 值对对象的属性进行存取操作,而不需要调用明确的存取方法(set 和 get 方法 )。基本上所有的 OC 对象都支持 KVC。
KVO : 即 Key-Value-Observing ,键值观察。回调机制,当指定的对象属性(内存地址/常量改变)被修改后,对象就会收到通知。

一 KVO基本使用

我们来看一段KVO基本使用的代码示例:

#import <UIKit/UIKit.h>
@interface People : NSObject
@property(assign,nonatomic) int age;
@end

#import "People.h"
@implementation People

@end

#import "ViewController.h"
#import "People.h"
@interface ViewController ()
@property(strong,nonatomic) People *people;

@end
@implementation ViewController
- (void)viewDidLoad {
    self.people = [[People alloc] init];
    self.people.age = 10;
    
    //给person对象添加KVO监听
    [self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

// 当监听对象的属性值发生改变时,就会调用这个方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == nil) {
        NSLog(@"监听到%@的%@的属性值改变了:%@",object,keyPath,change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc
{
    [self.people removeObserver:self forKeyPath:@"age"];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.people.age = 15;
}
@end
output:
2019-02-01 17:37:20.416444+0800 runloop[13965:867060] 监听到<People: 0x600001537530>的age的属性值改变了:{
    kind = 1;
    new = 15;
    old = 10;
}

我们调用对象addObserver方法给对象的某个属性添加监听,当对象的这个属性值改变的时候就会触发- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context方法,输出结果 值的变化内容都在change 中,KVO的使用是非常简单的,但是它的背后是如何实现的呢?我们一起来探索一下

二 KVO本质原理讲解及代码验证

我们看下面一段代码

#import <UIKit/UIKit.h>
@interface People : NSObject
@property(assign,nonatomic) int age;
@end

#import "People.h"

@implementation People
  
- (void)setAge:(int)age{
    _age = age;
}
@end
#import "ViewController.h"
#import "People.h"
@interface ViewController ()
@property(strong,nonatomic) People *people;
@property(strong,nonatomic) People *people2;

@end
@implementation ViewController
- (void)viewDidLoad {
    self.people = [[People alloc] init];
    self.people.age = 10;
    self.people2 = [[People alloc] init];
    self.people2.age = 20;
    
    //给person对象添加KVO监听
    [self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

// 当监听对象的属性值发生改变时,就会调用这个方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == nil) {
        NSLog(@"监听到%@的%@的属性值改变了:%@",object,keyPath,change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc
{
    [self.people removeObserver:self forKeyPath:@"age"];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.people setAge:15];
    [self.people2 setAge:30];
}


@end

我们知道self.people.age就等于调用[self.people setAge:],运行上面的程序,点击手机屏幕,我们可以发现它们都调用了people的setAge:方法,不同的是people触发了监听方法,而people2却没有,到了这里我们能确定的是:不管一个对象的属性有没有添加KVO监听,在修改对象属性的时候都会走对象的set方法,看来跟方法没有关系,那只有跟类有关系了。这里我们先给出结论,然后再逐一验证。

1 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
2 当修改instance对象的属性时,会调用Foundation的
_NSSetXXXValueAndNotify函数
//_NSSetXXXValueAndNotify函数内部实现

willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
3 didChangeValueForKey内部会触发监听器(Oberser)的监听方法observeValueForKeyPath:ofObject:change:context:

以上3个结论,我们先来验证第一个:就是当调用了下面这个添加属性监听方法

//给person对象添加KVO监听
    [self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

runtime会生成一个People的子类,并且让self.people的isa指向这个子类,其实就是让self.people的类对象替换成这个子类,那好我们来打印一下self.people,和self.people添加了属性监听后的类对象,我们来看一下它们还都是不是Person对象了。

 .......省略上面的代码

 NSLog(@"before class:%s",object_getClassName(self.people));
    //给person对象添加KVO监听
    [self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    NSLog(@"after class:%s",object_getClassName(self.people));
    NSLog(@"is People subclass:%d",[self.people isKindOfClass:[People class]]);
output:
2019-02-01 20:24:32.874514+0800 runloop[14845:939809] before class:People
2019-02-01 20:24:32.874816+0800 runloop[14845:939809] after class:NSKVONotifying_People
2019-02-01 20:24:32.874914+0800 runloop[14845:939809] is People subclass:1

通过程序运行我们可以看到,self.people在添加了属性监听后它的类对象确实变了,变成了NSKVONotifying_People,那么第一步我们验证完了,按我们再来验证修改这个子类的属性方法是不是调用了Foundation的
_NSSetXXXValueAndNotify函数,以及这个方法内部是不是调用了willChangeValueForKey:
Person的setter
didChangeValueForKey:
我们修改中间的代码 并且在最后一行打上断点 如下:

   .......省略上面的代码
    NSLog(@"before :%p",[self.people methodForSelector:@selector(setAge:)]);
    //给person对象添加KVO监听
    [self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    NSLog(@"after :%p",[self.people methodForSelector:@selector(setAge:)]);
2019-02-01 20:34:58.426171+0800 runloop[14942:945995] before :0x10301df70
2019-02-01 20:34:58.426464+0800 runloop[14942:945995] after :0x7fff257023ea
(lldb) p (IMP)0x10301df70
(IMP) $0 = 0x000000010301df70 (runloop`-[People setAge:] at People.m:13)
(lldb)  p (IMP)0x7fff257023ea
(IMP) $1 = 0x00007fff257023ea (Foundation`_NSSetIntValueAndNotify)
(lldb) 

我们通过methodForSelector:打印出self.people添加属性监听前后的setAge:方法的调用地址,然后利用lldb打印出调用地址的方法名,我们可以看出它确实调用了_NSSetIntValueAndNotify方法,那么_NSSetIntValueAndNotify内部流程是什么样的呢,我们要来验证一下,我们修改上面的代码如下:

#import "People.h"

@implementation People
  
- (void)setAge:(int)age{
    _age = age;
    NSLog(@"setAge");
}

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey%@",key);
}

- (void)didChangeValueForKey:(NSString *)key {
     NSLog(@"begin didChangeValueForKey%@",key);
    [super didChangeValueForKey:key];
    NSLog(@"end didChangeValueForKey%@",key);
}
@end
#import "ViewController.h"
#import "People.h"
@interface ViewController ()
@property(strong,nonatomic) People *people;
@property(strong,nonatomic) People *people2;

@end
@implementation ViewController
- (void)viewDidLoad {
    self.people = [[People alloc] init];
    self.people.age = 10;
    self.people2 = [[People alloc] init];
    self.people2.age = 20;
    
//    NSLog(@"before :%p",[self.people methodForSelector:@selector(setAge:)]);
//    //给person对象添加KVO监听
    [self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
//    NSLog(@"after :%p",[self.people methodForSelector:@selector(setAge:)]);
}

// 当监听对象的属性值发生改变时,就会调用这个方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == nil) {
        NSLog(@"监听到%@的%@的属性值改变了:%@",object,keyPath,change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc
{
    [self.people removeObserver:self forKeyPath:@"age"];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.people setAge:15];
//    [self.people2 setAge:30];
}

@end

运行起来后我们点击屏幕打印信息如下:

2019-02-01 20:49:52.671117+0800 runloop[15073:954069] willChangeValueForKeyage
2019-02-01 20:49:52.671519+0800 runloop[15073:954069] setAge
2019-02-01 20:49:52.671654+0800 runloop[15073:954069] begin didChangeValueForKeyage
2019-02-01 20:49:52.672014+0800 runloop[15073:954069] 监听到<People: 0x600002a9cda0>的age的属性值改变了:{
    kind = 1;
    new = 15;
    old = 10;
}
2019-02-01 20:49:52.672167+0800 runloop[15073:954069] end didChangeValueForKeyage

通过打印结果我们可以看到,正如我们结论所说,它先调用 willChangeValueForKey然后调用set最后调用didChangeValueForKey.并且这里我们也验证了第三个结论,3 didChangeValueForKey内部会触发监听器(Oberser)的监听方法observeValueForKeyPath:ofObject:change:context
OK 到此整个KVO底层实现流程我们也验证一遍了,最后我们在整体梳理下整个流程:

流程描述 (执行依次往下) 代码演示
1 给对象属性添加监听 [self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNewcontext:nil];
2 RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类 NSKVONotifying_People
3 当修改instance对象的属性时,会调用Foundation的 _NSSetXXXValueAndNotify函数 [self.people methodForSelector:@selector(setAge:)]
4 _NSSetXXXValueAndNotify函数会调用右边几个方法 1 willChangeValueForKey: 2 父类原来的setter 3 didChangeValueForKey:
5 didChangeValueForKey内部会触发监听器 - (void)didChangeValueForKey:(NSString *)key {[self observeValueForKeyPath:self ofObject:key change:nil context:nil];}

流程不算复杂就相当于生成一个新的子类重写它的set方法。最后简单介绍其他一些相关内容,

1 对象添加KVO监听前后的类结构图:
未使用KVO的对象
使用了KVO的对象
2 关于获取KVO对象类对象两个方法的差异
[self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    NSLog(@"class :%@",[self.people class]);
    NSLog(@"class :%s",object_getClassName(self.people));

虽然[ xx class]object_getClassName()都是获取isa指向的类对象,但是添加了KVO后的子类对象重写了class方法,它返回的是未添加KVO的类对象,而object_getClassName()是返回对象真实的类对象。
好了KVO就算讲完了,下面我们来看一下KVC相关的内容。

三 KVC基本使用

KVC是Key Value Coding的缩写,意思是键值编码。在iOS中,提供了一种方法通过使用属性的名称(也就是Key)来间接访问对象的属性方法。说的有的拗口,实际上就是通过类定义我们可以看到类的各种属性,那么使用属性的名称我们就能访问到类实例化后的对象的这个属性值。

这个方法可以不通过getter/setter方法来访问对象的属性。因为一个类的成员变量如果没有提供getter/setter的话,外界就失去了对这个变量的访问渠道。而KVC则提供了一种访问的方法。我们来看一下KVC的常用API

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;  

再来看一下KVC的基本使用

#import <UIKit/UIKit.h>
@interface People : NSObject
@property(assign,nonatomic) int age;
@property(strong,nonatomic) NSDictionary *dic;
@end

#import "People.h"

@implementation People
  

@end

#import "ViewController.h"
#import "People.h"
@interface ViewController ()
@property(strong,nonatomic) People *people;
@end
@implementation ViewController
- (void)viewDidLoad {
    
    self.people = [[People alloc] init];
    self.people.dic = [NSDictionary dictionaryWithObject:@20 forKey:@"age"];
   
    [self.people setValue:@10 forKey:@"age"];
    NSLog(@"%@",[self.people valueForKey:@"age"]);
    NSLog(@"%@",[self.people valueForKeyPath:@"dic.age"]);
}

@end
output:
2019-02-02 11:28:14.101638+0800 runloop[22631:1536170] 10
2019-02-02 11:28:14.101776+0800 runloop[22631:1536170] 20

其实KVC的使用也是很简单的,下面我们来看一下KVC的存取值原理

四 KVC设值原理

我们通过KVC给一个对象属性设置值经常使用setValue:forKey:下面让我们来看一下它的实现原理,下面先给一个调用流程图:

当我们调用setValue:forKey:方法后首先它会先调用这个对象相应属性的setXXX:方法,如果没有这个方法就会调用带下划线的_setXXX:方法,如果还没有就会查看这个对象类accessInstanceVariablesDirectly方法的返回值,返回false,就报NSUnknownKeyException异常,如果返回true,就按照,按照_key、_isKey、key、isKey顺序查找成员变量,并直接赋值。
下面让我们用代码来验证整个流程:

#import <UIKit/UIKit.h>
@interface People : NSObject
{
//    int _age;
//    int _isAge;
    int age;
    int isAge;
    
}
@end

#import "People.h"

@implementation People
  

//- (void) setAge:(int)age{
//    NSLog(@"setAge");
//}

//- (void) _setAge:(int)age{
//    NSLog(@"_setAge");
//}

+ (BOOL)accessInstanceVariablesDirectly
{
    return true;
}
@end
#import "ViewController.h"
#import "People.h"
@interface ViewController ()
@property(strong,nonatomic) People *people;
@end
@implementation ViewController
- (void)viewDidLoad {
    
    self.people = [[People alloc] init];
    [self.people setValue:@10 forKeyPath:@"age"];
}

@end

我们可以依次注释它的 set,_set,_key,_isKey,key,iskey,来查看它的执行情况。[setValue: forKeyPath:] 也是一样的执行流程,好了这就是KVC设置属性值的原理流程,下面我们看它取值的流程

五 KVC取值原理

当我们调用valueForKey:方法后首先它会先调用这个对象相应属性的getKey、key、 isKey、_key,如果还都没有就会查看这个对象类 accessInstanceVariablesDirectly方法的返回值,返回false,就报NSUnknownKeyException异常,如果返回true,就按照,按照_key、_isKey、key、isKey顺序查找成员变量,并直接取值。

KVC取值原理流程图如下:


KVC取值原理流程验证示例代码

#import <UIKit/UIKit.h>
@interface People : NSObject
{
    @public
    int _age;
    int _isAge;
    int age;
    int isAge;
    
}
@end

#import "People.h"

@implementation People
  

//- (int) getAge{
//    NSLog(@"getAge");
//    return 10;
//}

//- (int) age{
//    NSLog(@"age");
//    return 20;
//}

//- (int) isAge{
//    NSLog(@"isAge");
//    return 30;
//}

- (int) _age{
    NSLog(@"_age");
    return 40;
}

+ (BOOL)accessInstanceVariablesDirectly
{
    return true;
}
@end

#import "ViewController.h"
#import "People.h"
@interface ViewController ()
@property(strong,nonatomic) People *people;
@end
@implementation ViewController
- (void)viewDidLoad {
    
    self.people = [[People alloc] init];
    self.people->_age = 10;
    self.people->_isAge = 20;
    self.people->age = 30;
    self.people->isAge = 40;
    
    [self.people valueForKey:@"age"];
}

@end

到此KVC的存取值虽然看这个调用的方法很多,但是流程都是大同小意的,还是有规律可循的,KVC原理我们就算讲完了,下面我们来看几个问题:

1 如何手动触发KVO?

手动调用willChangeValueForKey:和didChangeValueForKey:

2 直接修改成员变量会触发KVO么?

不会,描述如下

@interface People : NSObject
{
    @public
    int _age;
    int _isAge;
    int age;
    int isAge;
    
}
@end


- (void)viewDidLoad {
    
    self.people = [[People alloc] init];
    self.people->_age = 10;
    self.people->_isAge = 20;
    self.people->age = 30;
    self.people->isAge = 40;
}

这种情况不会触发,这种情况没有调用对象的set方法,是直接给属性赋值。

3 通过KVC修改属性会触发KVO么?

会触发KVO,自己写代码可以验证,我们猜测KVC在setValue: forKeyPath:方法中手动调用了属性监听回调observeValueForKeyPath:ofObject:change:context方法,有人说KVO基于KVC,我觉得不需要,只要你在调用KVC setValue :forKey方法时,它内部再调用一下KVO的通知方法就好了就像下面伪代码这样,我也不管你对象有没有调用set方法,就能实现通知。

- (void)setValue:(id)value forKey:(NSString *)key{
    self.people->_age = value;
    [self observeValueForKeyPath:key ofObject:value change:nil context:nil];
}

OK关于KVC,KVO我们就讲到这,内容不多,需要我们在开发中多多实践。

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

推荐阅读更多精彩内容