一 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监听前后的类结构图:
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我们就讲到这,内容不多,需要我们在开发中多多实践。