KVO 的基本概念(Key Value Observing)
基本概念
鍵值觀察是一種使對象獲取其他對象的特定屬性變化的通知機制昵观。控制器層的綁定技術就是嚴重依賴鍵值觀察獲得模型層和控制器層的變化通知的舌稀。對于不依賴控制器層類的應用程序啊犬,鍵值觀察提供了一種簡化的方法來實現(xiàn)檢查器并更新用戶界面值壁查。
與NSNotification不同觉至,鍵值觀察中并沒有所謂的中心對象來為所有觀察者提供變化通知。取而代之地睡腿,當有變化發(fā)生時语御,通知被直接發(fā)送至處于觀察狀態(tài)的對象。NSObject提供這種基礎的鍵值觀察實現(xiàn)方法席怪,你幾乎不用重寫該方法应闯。
你可以觀察任意對象屬性,包括簡單屬性挂捻,對一或對多關系碉纺。對多關系的觀察者將會被告知發(fā)生變化的類型,也就是任意發(fā)生變化的對象刻撒。
鍵值觀察為所以對象提供自動觀察兼容性骨田。你可以通過禁用自動觀察通知并實現(xiàn)手動通知來篩選通知。注冊觀察者
為了正確接收屬性的變更通知声怔,觀察對象必須首先發(fā)送一個addObserver: forKeyPath: options: context: 消息至被觀察對象态贤,用以傳送觀察對象和需要觀察的屬性的關鍵路徑,以便于其注冊醋火。選項參數(shù)指定了發(fā)送變更通知時提供給觀察者的信息悠汽。使用NSKeyValueObservingOptionOld選項可以將初始對象值以變更字典中的一個項的形式提供給觀察者箱吕。指定NSKeyValueObservingOptionNew選項可以將新的值以一個項的形式添加至變更字典。你可以用逐為"|"同時使用這兩個常量來指定上述兩種類型的值柿冲。
person.name = @"xiao wang";改變姓名 person.name = @"xiao ming";
/*
作用:給對象綁定一個觀察者(監(jiān)聽者)
addObserver: 觀察者
forKeyPath: 要監(jiān)聽的屬性
options: 選項(方法中拿到屬性值)
context: 上下文一般為nil
*/
//監(jiān)聽變更舊的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld context:nil];
此時打又呈稀:
change = {
kind = 1;
old = "xiao wang";
}
//監(jiān)聽變更新的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
此時打印:
change = {
kind = 1;
new = "xiao ming";
}
//同時監(jiān)聽變更舊的值和新的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
此時打印:
change = {
kind = 1;
new = "xiao ming";
old = "xiao wang";
}
- 接收變更通知
當監(jiān)聽的屬性發(fā)生變動時,觀察者收到observeValueForKeyPath: ofObject: change: context: 消息姻采,觀察者必須實現(xiàn)這一方法。觸發(fā)觀察者通知的對象和鍵路徑爵憎、包含變更細節(jié)的字典慨亲,以及觀察者注冊時提交的上下文指針均被提交給觀察者
/**
* 當監(jiān)聽的屬性值發(fā)生改變是執(zhí)行
*
* @param keyPath 發(fā)生改變的屬性
* @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";
}
- 移除觀察者身份
你可以發(fā)送一條指定指定觀察者對象和鍵路徑的removeObserver: forKeyPath: 消息至被觀察的對象,來移除一個鍵值觀察者宝鼓。//移除觀察者身份 [person removeObserver:self forKeyPath:@"name"];
KVO 使用需要注意的一些地方
- 在對象銷毀時要先移除觀察者身份刑棵,否則會報錯
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
person.name = @"xiao wang";
//監(jiān)聽變更新的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
person.name = @"xiao ming";
}
/**
* 當監(jiān)聽的屬性值發(fā)生改變是執(zhí)行
*
* @param keyPath 發(fā)生改變的屬性
* @param object 改變的屬性所屬的對象
* @param change 改變的內容
* @param context 上下文
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
// 打印改變的內容
NSLog(@"change = %@",change);
}
在執(zhí)行完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 ???áaá?§?áêü 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 ???áaá?§?áêü 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";
//監(jiān)聽變更新的值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
person.name = @"xiao ming";
//移除觀察者身份
[person removeObserver:self forKeyPath:@"name"];
}
- 修改觀察的屬性的值需要用“.”語法去修改或者KVC蛉签,改下劃線屬性(__examTime),KVO并不能監(jiān)聽到
創(chuàng)建一個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 方法中監(jiān)聽
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
是可以監(jiān)聽的到時間變化的
*/
//_examTime--;
/*
在ViewController 的viewDidLoad 方法中監(jiān)聽
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
是不能監(jiān)聽的到時間變化的
*/
//[self setValue:[NSNumber numberWithInt:_examTime] forKey:@"examTime"];
/*
在ViewController 的viewDidLoad 方法中監(jiān)聽
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
是可以監(jiān)聽的到時間變化的
*/
//[self setValue:[NSNumber numberWithInt:_examTime] forKey:@"_examTime"];
/*
在ViewController 的viewDidLoad 方法中監(jiān)聽
Student *student = [[Student alloc] init];
[student addObserver:self forKeyPath:@"examTime" options:NSKeyValueObservingOptionOld context:nil];
是不能監(jiān)聽的到時間變化的原因是KVO注冊監(jiān)聽的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--不能觸發(fā)觀察者模式
固不作進一步優(yōu)化
*/
}
/**
* 當監(jiān)聽的屬性值發(fā)生改變是執(zhí)行
*
* @param keyPath 發(fā)生改變的屬性
* @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;
}