iOS 中监听某个值的改变有哪些方法?
在一个复杂的,有状态的系统中,当一个对象的状态发生改变,如何通知系统,并对状态改变做出相应的行为是必需考虑的一个问题,在iOS中为这类问题提供了4种解决方法:
NSNotifiactaion 和 NSNotificationCenter:通知中心
Delegates:代理,
Callback:回调,
KVO(Key-Value Observing):键值观察
Key-Value Observing (简写为KVO):当指定的对象的属性被修改了,允许对象接受到通知的机制。每次指定的被观察对象的属性被修改的时候,KVO都会自动的去通知相应的观察者。
KVO 是什么?
Objective-C 中的键(key)-值(value)观察(KVO)并不是什么新鲜事物,它来源于设计模式中的观察者模式,其基本思想就是:
一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。
KVO 的用法
- 如果要监听“对象A”属性值的改变,先要为"对象A"的属性注册观察者(假设观察者为“对象B”)。
- (void)addObserver:(NSObject * _Nonnull)anObserver
forKeyPath:(NSString * _Nonnull)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context
anObserver
: 观察者,注册 KVO 通知的对象. 其必须实现方法 observeValueForKeyPath:ofObject:change:context:.
keyPath
: 被观察的属性,其不能为nil.
options
: 表示要监听那些通知,一般为写 0
context
: 一些其他的需要传递给观察者的上下文信息,通常设置为 nil
options
解释如下:
NSKeyValueObservingOptionNew
: change 字典中包含 key 改变后的新值
NSKeyValueObservingOptionOld
: change 字典中包含 key 改变前的旧值
NSKeyValueObservingOptionInitial
: 在添加观察者的时候立即发送一个通知给观察者,并且是在注册观察者方法返回之前
NSKeyValueObservingOptionPrior
: 如果指定,则在每次修改属性时,会在修改通知被发送之前预先发送一条通知给观察者,这与-willChangeValueForKey:被触发的时间是相对应的。这样,在每次修改属性时,实际上是会发送两条通知。
2. 观察者“对象B”实现 observeValueForKeyPath:ofObject:change:context:
. 方法
3. “对象A”移除监听者 - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
;
另外注意:keyPath
即被观察的属性只能是 NSString
类型
- 注意点:在哪个线程触发监听(修改了值),监听方法就会在哪个线程中执行
KVO 的实现原理
键值编码和键值观察是根据 isa-swizzling 技术来实现的,主要依据runtime的强大动态能力。
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。派生类在被重写的 setter 方法实现真正的通知机制。
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
- 重写新类的 class 方法
重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。
//打印如下内容:
NSLog(@"self->isa:%@",self->isa);
NSLog(@"self class:%@",[self class]);
//在建立KVO监听前,打印结果为:
self->isa:Person
self class:Person
//在建立KVO监听之后,打印结果为:
self->isa:NSKVONotifying_Person
self class:Person
- 重写新类的 set 方法
新类会重写对应的set方法,是为了在set方法中增加另外两个方法的调用:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
其中,didChangeValueForKey
:方法负责调用:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
如果没有 setter 方法,那么 -setValue:forKey 方法会直接调用:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
如果在没有使用键值编码且没有使用适当命名的访问起方法的时候,我们只需要显示调用上述两个方法,同样可以使用KVO!
例子
//
// ViewController.m
// KVO test
//
// Created by KeSen on 15/9/1.
// Copyright (c) 2015年 KeSen. All rights reserved.
//
#import "ViewController.h"
#import "KSBaby.h"
@interface ViewController ()
{
KSBaby *_baby;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
KSBaby *baby = [[KSBaby alloc] initWithHungry:@"cryed" thirst:NO];
_baby = baby;
// 一般如下使用
// [_baby addObserver:self forKeyPath:@"cry" options: 0 context: nil];
// 在 self 中监听 _baby 的 cry 属性变化
[_baby addObserver:self forKeyPath:@"cry" options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior context:@"ssss"];
}
- (void)dealloc {
[_baby removeObserver:self forKeyPath:@"cry"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"keyPath: %@\n change: %@\n context: %@\n", keyPath, change, context);
}
- (IBAction)click:(UIButton *)sender {
_baby.cry = @"crying";
}
@end
//
// ViewController.m
// KVO test
//
// Created by KeSen on 15/9/1.
// Copyright (c) 2015年 KeSen. All rights reserved.
//
#import "ViewController.h"
#import "KSBaby.h"
@interface ViewController ()
{
KSBaby *_baby;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
KSBaby *baby = [[KSBaby alloc] initWithHungry:@"cryed" thirst:NO];
_baby = baby;
// 一般如下使用
// [_baby addObserver:self forKeyPath:@"cry" options: 0 context: nil];
// 在 self 中监听 _baby 的 cry 属性变化
[_baby addObserver:self forKeyPath:@"cry" options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior context:@"ssss"];
}
- (void)dealloc {
[_baby removeObserver:self forKeyPath:@"cry"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"keyPath: %@\n change: %@\n context: %@\n", keyPath, change, context);
}
- (IBAction)click:(UIButton *)sender {
_baby.cry = @"crying";
}
@end
那么:程序启动时输出:
点击按钮后输出:
KVO与多线程
注意点:在哪个线程触发监听(修改了值),监听方法就会在哪个线程中执行
//
// ViewController.m
// KVO test
//
// Created by KeSen on 15/9/1.
// Copyright (c) 2015年 KeSen. All rights reserved.
//
#import "ViewController.h"
#import "KSBaby.h"
#import "FBKVOController.h"
@interface ViewController ()
{
KSBaby *_baby;
FBKVOController *_observer;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
KSBaby *baby = [[KSBaby alloc] initWithHungry:@"cryed" thirst:NO];
_baby = baby;
// NSKeyValueObservingOptionNew : change 字典中包含 key 改变后的新值
// NSKeyValueObservingOptionOld : change 字典中包含 key 改变前的旧值
// NSKeyValueObservingOptionInitial : 在添加观察者的时候立即发送一个通知给观察者,并且是在注册观察者方法返回之前
// NSKeyValueObservingOptionPrior : 如果指定,则在每次修改属性时,会在修改通知被发送之前预先发送一条通知给观察者,这与-willChangeValueForKey:被触发的时间是相对应的。这样,在每次修改属性时,实际上是会发送两条通知。
// 1. 一般用法如下
[_baby addObserver:self forKeyPath:@"cry" options: 0 context: nil];
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// [_baby addObserver:self forKeyPath:@"cry" options: NSKeyValueObservingOptionNew context:@"ssss"];
//
// });
// 2. 第三方库 FBKVOController 的用法
_observer = [[FBKVOController alloc] initWithObserver:self];
[_observer observe:_baby keyPath:@"cry" options:NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
NSLog(@"%@, %@", @"FBKVOController", change[NSKeyValueChangeNewKey]);
}];
}
- (void)dealloc {
[_baby removeObserver:self forKeyPath:@"cry"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"keyPath: %@\n change: %@\n context: %@\n", keyPath, change, context);
}
- (IBAction)click:(UIButton *)sender {
// _baby.cry = @"crying";
// 1. 注意点:在哪个线程触发监听(修改了值),监听方法就会在哪个线程中执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_baby.cry = @"cry again";
});
}
@end
KVO 监听 NSMutableArray 的内容变化
- 将数组封装到一个对象中
- 给这个对象添加 KVO 监听
-
使用 [self.arrayObject mutableArrayValueForKey:@"array”] 获取数组对象,对数组对象今天添加删除操作,只有这样触发的数组才会触发 kvo
参考:http://www.cppblog.com/kesalin/archive/2012/11/17/kvo.html
http://ningandjiao.iteye.com/blog/2009729
http://www.bkjia.com/IOSjc/993206.html
http://blog.csdn.net/wzzvictory/article/details/9674431