KVC和KVO的使用及原理
关于KVC
KVC是什么?
Key-Value Coding,即键值编码。它是一种不通过存取方法,而通过属性名称字符串间接访问属性的机制。
KVC常用的方法
前两个方法无论获取值还是赋值,只需要传入属性名称的字符串就行了。但KVC也提供了传入path
的方法。所谓path,就是用点号连接的多层级的属性,比如student.name
,student属性里的name属性。
- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
KVC对多种数据类型的支持
首先要说的是对于基本数据类型的属性,KVC的这几个方法会自动装箱和拆箱。
其次,KVC也支持数组和字典等集合数据。这里不多探讨,关于这些知识可以看这篇博文:KVC/KVO原理详解及编程指南
KVC的原理:KVC是怎么访问属性的?
KVC在某种程度上提供了替代存取方法(访问器方法)的方案,不过存取方法终究是个好东西,以至于只要有可能,KVC也尽可能先尝试使用存取方法访问属性。当使用KVC访问属性时,它内部其实做了很多事:
1.首先查找有无<property>,set<property>,is<property>等property属性对应的存取方法,若有,则直接使用这些方法;
2.若无,则继续查找_<property>,_get<property>,set<property>等方法,若有就使用;
3.若查询不到以上任何存取方法,则尝试直接访问实例变量<property>,<property>;
4.若连该成员变量也访问不到,则会在下面方法中抛出异常。之所以提供这两个方法,就是让你在因访问不到该属性而程序即将崩掉前,供你重写,在内做些处理,防止程序直接崩掉。
valueForUndefinedKey:
和setValue:forUndefinedKey:
方法。
关于KVO
KVO是什么?
Key-Value Obersver,即键值观察。它是观察者模式的一种衍生。基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,会自动的通知观察者。这里所谓的通知是触发观察者对象实现的KVO的接口方法。
** KVO是解决model和view同步的好法子。**
另外,KVO的优点是当被观察的属性值改变时是会自动发送通知的,这比通知中心需要post通知来说,简单了许多。
KVO怎么用?
1.首先给目标对象的属性添加观察:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2.实现下面方法来接收通知,需要注意各个参数的含义:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;
3.最后要移除观察者:
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
举一个🌰:
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
{
Student *_student;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_student = [[Student alloc] init];
_student.stuName = @"oldName_hu";
// 1.给student对象的添加观察者,观察其stuName属性
[_student addObserver:self forKeyPath:@"stuName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
// 此时,stuName发生了变化
_student.stuName = @"newName_wang";
}
// stuName发生变化后,观察者(self)立马得到通知。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
// 最好判断目标对象object和属性路径keyPath
if(object == _student && [keyPath isEqualToString:@"stuName"])
{
NSLog(@"----old:%@----new:%@",change[@"old"],change[@"new"]);
}else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
// 移除观察者
[_student removeObserver:self forKeyPath:@"stuName"];
}
@end
上面代码需要注意的一点是接收通知的方法里最好判断一下目标对象和属性路径,因为有可能有多个KVO观察多个对象的属性;而且,父类也有可能使用了KVO哦,所以在else里,对现有条件外的情况交给父类去处理。
KVO的原理:KVO是怎么实现的?
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中被观察属性的 setter 方法,在setter方法里使其具有通知机制。因此,要想KVO生效,必须直接或间接的通过setter方法访问属性(KVC的setValue就是间接方式)。直接访问成员变量KVO是不生效的。
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
重新的setter方法里到底干了什么,而使其就有了通知机制呢?其实只是在setter方法里,给属性赋值的前后分别调用了两个方法
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
而- (void)didChangeValueForKey:(NSString *)key;
会调用
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;
这就是KVO实现的基本原理了!
当没有存取方法而通过KVC的setValue修改属性值时,同样的在运行时也会在setValue:forKey方法里默认调用上面俩方法。
** 其实我们也可以手动,显式的调用这两个方法,以使其具有通知机制。**
下面用例子验证:
#import "ViewController.h"
@interface ViewController ()
{
NSString *_testStr;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 给self的添加self观察者,自己观察自己的testStr成员变量
[self addObserver:self forKeyPath:@"testStr" options:NSKeyValueObservingOptionNew context:nil];
[self willChangeValueForKey:@"testStr"];
_testStr = @"this is a test"; // 直接修改成员变量的值,但是显式的、手动的调用上下俩方法,使其就有通知机制
[self didChangeValueForKey:@"testStr"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if(object == self && [keyPath isEqualToString:@"testStr"])
{
NSLog(@"----new:%@----",change[@"new"]);
}else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
// 移除观察者
[self removeObserver:self forKeyPath:@"stuName"];
}
@end