相同点:
- 两者都是用于监听,且实现原理都是
观察者模式
; - 都是
一对多
.
不同点:
-
KVO只能用于监听对象属性的变化
,并且属性名都是通过NSString
来查找,因为都是字符串,手敲过程中容易出错,编译时不会报错; -
NSNotification
的发送监听(post)
的操作是我们通过代码控制的,KVO是系统控制的
; -
KVO
可以记录新旧值的变化.
KVO注意事项
基本用法:
KVO的使用主要分为3步:
- 注册观察者
addObserver:forKeyPath:options:context
;
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- 实现KVO回调observeValueForKeyPath:ofObject:change:context
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change);
}
}
- 移除观察者removeObserver:forKeyPath:context
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
大致含义就是:addObserver:forKeyPath:options:context:
方法中的上下文context
指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以通过指定context为NULL
,从而依靠keyPath即键路径字符串
传来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径
而导致问题。所以可以为每个观察到的keyPath创建一个不同的context,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析
通俗的讲,context上下文主要是用于区分不同对象的同名属性
,从而在KVO回调方法中可以直接使用context进行区分,可以大大提升性能,以及代码的可读性
使用context区分通知来源:
//定义context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
//注册观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"%@",change);
}else if (context == PersonNameContext){
NSLog(@"%@",change);
}
}
关于KVO的自动触发与手动触发:
KVO观察的开启和关闭有两种方式,自动
和手动
,可以灵活设置监听模式:
- 自动开关,返回NO,就监听不到,返回YES,表示监听
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
- 自动开关关闭的时候,可以通过手动开关监听
- (void)setName:(NSString *)name{
//手动开关
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
KVO"一对多"观察:
以下载进度为例,比如目前有一个需求,需要根据总的下载量totalData
和当前下载量currentData
来计算当前的下载进度currentProcess
:
- 实现
keyPathsForValuesAffectingValueForKey
方法,将两个观察合为一个观察,即观察当前下载进度currentProcess
//1、合二为一的观察方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"currentProcess"]) {
NSArray *affectingKeys = @[@"totalData", @"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
//2、注册KVO观察
[self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];
//3、触发属性值变化
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person.currentData += 10;
self.person.totalData += 1;
}
//4、移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"currentProcess"];
}
KVO观察可变数组
在KVC官方文档中,针对可变数组的集合类型,有如下说明,即访问集合对象需要通过mutableArrayValueForKey
方法,这样才能将元素添加到可变数组中.
为什么呢?
因为KVO是基于KVC基础之上的,所以可变数组如果直接添加数据,是不会调用setter方法的,所有对可变数组
的KVO观察下面这种方式不生效的,即直接通过[self.person.dateArray addObject:@"1"]
;向数组添加元素,是不会触发kvo通知回调的
//1、注册可变数组KVO观察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
//2、KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"%@",change);
}
//3、移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
//4、触发数组添加数据
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
//无法触发KVO监听
// [self.person.dateArray addObject:@"1"];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
其中的kind
表示键值变化的类型:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//设值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替换
};
移除KVO通知的重要性
删除观察者时,请记住以下几点:
要求被移除为观察者(如果尚未注册为观察者)会导致
NSRangeException
。您可以对removeObserver:forKeyPath:context:
进行一次调用,以对应对addObserver:forKeyPath:options:context:
的调用,或者,如果在您的应用中不可行,则将removeObserver:forKeyPath:context:
调用在try / catch
块内处理潜在的异常。释放后,观察者不会自动将其自身移除
。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前将自己删除
。该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在
init
或viewDidLoad
中)注册为观察者,并在释放过程中(通常在dealloc
中)注销,以确保成对和有序地添加和删除消息
,并确保观察者在注册之前被取消注册
,从内存中释放出来
。
所以,总的来说,KVO注册观察者 和移除观察者是需要成对出现的
,如果只注册,不移除,会出现类似野指针的崩溃
,如下图所示