KVO 的基本概念(Key Value Observing)
基本概念
键值观察是一种使对象获取其他对象的特定属性变化的通知机制。控制器层的绑定技术就是严重依赖键值观察获得模型层和控制器层的变化通知的。对于不依赖控制器层类的应用程序,键值观察提供了一种简化的方法来实现检查器并更新用户界面值。
与NSNotification不同,键值观察中并没有所谓的中心对象来为所有观察者提供变化通知。取而代之地,当有变化发生时,通知被直接发送至处于观察状态的对象。NSObject提供这种基础的键值观察实现方法,你几乎不用重写该方法。
你可以观察任意对象属性,包括简单属性,对一或对多关系。对多关系的观察者将会被告知发生变化的类型,也就是任意发生变化的对象。
键值观察为所以对象提供自动观察兼容性。你可以通过禁用自动观察通知并实现手动通知来筛选通知。注册观察者
为了正确接收属性的变更通知,观察对象必须首先发送一个addObserver: forKeyPath: options: context: 消息至被观察对象,用以传送观察对象和需要观察的属性的关键路径,以便于其注册。选项参数指定了发送变更通知时提供给观察者的信息。使用NSKeyValueObservingOptionOld选项可以将初始对象值以变更字典中的一个项的形式提供给观察者。指定NSKeyValueObservingOptionNew选项可以将新的值以一个项的形式添加至变更字典。你可以用逐为"|"同时使用这两个常量来指定上述两种类型的值。
person.name = @"xiao wang";改变姓名 person.name = @"xiao ming";
/*
作用:给对象绑定一个观察者(监听者)
addObserver: 观察者
forKeyPath: 要监听的属性
options: 选项(方法中拿到属性值)
context: 上下文一般为nil
*/
//监听变更旧的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld context:nil];
此时打印:
change = {
kind = 1;
old = "xiao wang";
}
//监听变更新的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
此时打印:
change = {
kind = 1;
new = "xiao ming";
}
//同时监听变更旧的值和新的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
此时打印:
change = {
kind = 1;
new = "xiao ming";
old = "xiao wang";
}
- 接收变更通知
当监听的属性发生变动时,观察者收到observeValueForKeyPath: ofObject: change: context: 消息,观察者必须实现这一方法。触发观察者通知的对象和键路径、包含变更细节的字典,以及观察者注册时提交的上下文指针均被提交给观察者
/**
* 当监听的属性值发生改变是执行
*
* @param keyPath 发生改变的属性
* @param object 改变的属性所属的对象
* @param change 改变的内容
* @param context 上下文
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
// 打印改变的内容
NSLog(@"change = %@",change);
}
打印结果如:
change = {
kind = 1;
new = "xiao ming";
old = "xiao wang";
}
- 移除观察者身份
你可以发送一条指定指定观察者对象和键路径的removeObserver: forKeyPath: 消息至被观察的对象,来移除一个键值观察者。//移除观察者身份 [person removeObserver:self forKeyPath:@"name"];
KVO 使用需要注意的一些地方
- 在对象销毁时要先移除观察者身份,否则会报错
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
person.name = @"xiao wang";
//监听变更新的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
person.name = @"xiao ming";
}
/**
* 当监听的属性值发生改变是执行
*
* @param keyPath 发生改变的属性
* @param object 改变的属性所属的对象
* @param change 改变的内容
* @param context 上下文
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
// 打印改变的内容
NSLog(@"change = %@",change);
}
在执行完viewDidLoad方法后,person会被销毁。然而viewDidLoad在person不需要再使用的时候并没有移除person的观察者身份会引起crash
change = {
kind = 1;
new = "xiao ming";
}
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x7fa633d97950 of class Person was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x7fa633d920d0> (
<NSKeyValueObservance 0x7fa633d9ad80: Observer: 0x7fa633e13090, Key path: name, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x7fa633d9ac40>
)'
*** First throw call stack:
(
0 CoreFoundation 0x000000010ca92e65 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x000000010c50bdeb objc_exception_throw + 48
2 CoreFoundation 0x000000010ca92d9d +[NSException raise:format:] + 205
3 Foundation 0x000000010c11d611 NSKVODeallocate + 294
4 libobjc.A.dylib 0x000000010c51fafe _ZN11objc_object17sidetable_releaseEb + 232
5 图片处理 0x000000010c0089bc -[ViewController viewDidLoad] + 252
6 UIKit 0x000000010cfd5f98 -[UIViewController loadViewIfRequired] + 1198
7 UIKit 0x000000010cfd62e7 -[UIViewController view] + 27
8 UIKit 0x000000010ceacab0 -[UIWindow addRootViewControllerViewIfPossible] + 61
9 UIKit 0x000000010cead199 -[UIWindow _setHidden:forced:] + 282
10 UIKit 0x000000010cebec2e -[UIWindow makeKeyAndVisible] + 42
11 UIKit 0x000000010ce37663 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4131
12 UIKit 0x000000010ce3dcc6 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1760
13 UIKit 0x000000010ce3ae7b -[UIApplication workspaceDidEndTransaction:] + 188
14 FrontBoardServices 0x000000010f80b754 -[FBSSerialQueue _performNext] + 192
15 FrontBoardServices 0x000000010f80bac2 -[FBSSerialQueue _performNextFromRunLoopSource] + 45
16 CoreFoundation 0x000000010c9bea31 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
17 CoreFoundation 0x000000010c9b495c __CFRunLoopDoSources0 + 556
18 CoreFoundation 0x000000010c9b3e13 __CFRunLoopRun + 867
19 CoreFoundation 0x000000010c9b3828 CFRunLoopRunSpecific + 488
20 UIKit 0x000000010ce3a7cd -[UIApplication _run] + 402
21 UIKit 0x000000010ce3f610 UIApplicationMain + 171
22 图片处理 0x000000010c00a51f main + 111
23 libdyld.dylib 0x000000010f1ce92d start + 1
24 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
修复viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
person.name = @"xiao wang";
//监听变更新的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
person.name = @"xiao ming";
//移除观察者身份
[person removeObserver:self forKeyPath:@"name"];
}
- 修改观察的属性的值需要用“.”语法去修改或者KVC,改下划线属性(__examTime),KVO并不能监听到
创建一个Student类
Student.h文件
#import <Foundation/Foundation.h>
@interface Student : NSObject
// 离考试时间
@property (nonatomic, assign) int examTime;
@end
Student.m文件
#import "Student.h"
@implementation Student
/**
* 初始化方法
*
* @return <#return value description#>
*/
- (instancetype)init{
self = [super init];
if (self != nil) {
self.examTime = 10;
//设置一个定时器,减少离考试的时间examTime
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeGone:) userInfo:nil repeats:YES];
};
return self;
}
//定时器方法
- (void)timeGone:(NSTimer *)timer{
self.examTime--;
/*
self.examTime--;
在ViewController 的viewDidLoad 方法中监听
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
是可以监听的到时间变化的
*/
//_examTime--;
/*
在ViewController 的viewDidLoad 方法中监听
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
是不能监听的到时间变化的
*/
//[self setValue:[NSNumber numberWithInt:_examTime] forKey:@"examTime"];
/*
在ViewController 的viewDidLoad 方法中监听
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
是可以监听的到时间变化的
*/
//[self setValue:[NSNumber numberWithInt:_examTime] forKey:@"_examTime"];
/*
在ViewController 的viewDidLoad 方法中监听
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
是不能监听的到时间变化的原因是KVO注册监听的key是@"examTime", KCV改变的key是@"_examTime"
*/
}
@end
在ViewController的viewDidLoad方法中使用
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
/*
这里的代码存在问题
没有添加移除student观察者身份,但此处只是为了验证_examTime--不能触发观察者模式
固不作进一步优化
*/
}
/**
* 当监听的属性值发生改变是执行
*
* @param keyPath 发生改变的属性
* @param object 改变的属性所属的对象
* @param change 改变的内容
* @param context 上下文
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
// 打印改变的内容
NSLog(@"change = %@",change);
}
打印结果:
change = {
kind = 1;
old = 10;
}
change = {
kind = 1;
old = 9;
}
change = {
kind = 1;
old = 8;
}
change = {
kind = 1;
old = 7;
}