本文为L_Ares个人写作,以任何形式转载请表明原文出处。
前两节即然说了KVC
,那么接下来一定是基于KVC
出现的KVO
了。
一、KVO基本简介
-
英文全称 :
Key-Value Observing
-
中文全称 : 键值观察
-
作用 :
KVO
是一种机制,允许在对象的指定属性
(看好是属性,没说成员变量)发生更改时通知对象,也可以自己观察自己的指定属性
。 -
官方建议 : 应用程序中模型层和控制器层之间的通信。
-
使用范围 : 但凡是继承了
NSObject
的类,都可以用。可以监听简单类型
,也可以监听集合类型
。 -
经常被拿过来对比的对象 :
NSNotificatioCenter
- 相同点 :
- 实现原理都是观察者模式。
- 都是可以进行一对多的通知。
- 不同点 :
-
KVO
监听的是对象的属性。NSNotificatioCenter
监听的范围就大了。 -
KVO
发送监听的动作是由系统来进行的。NSNotificatioCenter
则可以利用postNotification
方法进行自己的掌控。 -
KVO
可以记录属性的旧有值和新值的变化。s -
KVO
使用完了必须销毁
。NSNotificatioCenter
在iOS9
以后对已经销毁的监听器不会发送通知了,也不会对已经销毁的被监听对象发送消息,从而不会出现野指针的错误。
-
- 相同点 :
-
学习前提 : 想要了解
KVO
的原理,必须要先学习好KVC
的原理。
二、KVO的基本操作API
这个API
就按照一般情况下的使用流程来说。
(1). 注册观察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
参数解析 :
-
首先,谁调用了这个方法,谁就是
被观察者
。比如 :JDPerson
的对象person
调用了该方法,那么person
就会给自己添加一个观察者,person
就是被观察者
。 -
observer
: 官方原话 : 为KVO通知注册的对象。说白了就是观察者
。
观察者
必须实现observeValueForKeyPath:ofObject:change:context:
方法。 -
keyPath
: 官方原话 : 要观察的属性的路径(相对接收此消息的对象)。说白了就是被观察者的属性名称或路径
。这个值不允许为nil
。 -
options
: 官方原话 :NSKeyValueObservingOptions
值的组合,它指定在观察通知中包含什么。- 下面说一下
NSKeyValueObservingOptions
这个枚举都有什么。-
NSKeyValueObservingOptionNew
: 指示更改字典应该提供新的属性值(如果适用)。 -
NSKeyValueObservingOptionOld
: 指示更改字典应该包含旧的属性值(如果适用)。 -
NSKeyValueObservingOptionInitial
: 在观察者注册方法返回之前,应该立即向观察者发送一个通知。 -
NSKeyValueObservingOptionPrior
: 在每次更改之前和之后,都向观察者发送单独的通知,而不是在更改之后发送单个通知。
-
- 下面说一下
-
context
: 官方原话 : 通过observeValueForKeyPath:ofObject:change:context:
方法传递给观察者
的任意数据。
(2). 观察回调方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
方法解析 :
这个方法是观察者
必须要实现的方法,在上面的(1)中的observer
已经说了。这是一个回调方法,在被观察者
的特定属性
发生了改变之后,观察者
通过这个方法得到通知。
参数解析 :
-
keyPath
:被观察者
发生更改的值的路径。 -
object
:被观察者
。 -
change
: 一个字典,描述被观察者的属性值
所做的更改。 -
context
: 在注册观察者的时候提供的context
值。一般拿来判断是哪个被观察者
的属性
发生了改变。
(3). 移除观察者
方法解析 :
KVO
在观察者或被观察者
释放之前,必须移除观察者。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
参数解析 :
-
首先,谁注册的
观察者
,谁就要移除观察者
。比如上面是person
注册的观察者
,那么peron
就要在观察者
的delloc
里面调用这个方法。 -
observer
:观察者
。 -
keyPath
:被观察者
的被观察的属性名称或路径。
一个小Tip :
移除观察者是在注册观察者之后要进行的事情,如果没有注册观察者就调用移除方法,则会出现
NSRangeException
。如果你不知道是否对某个对象的某个属性注册了观察者,可以在你认为可能注册的观察者的delloc
中使用try/catch
,然后尝试移除。
(4). 关于Context
这里要重点说一下这个Context
,很多人都是在注册观察者的时候,直接给这个Context
赋值为nil
,如果不需要使用的话,赋值nil
是没问题的,但是还是尽量写成NULL
,因为从上述的API
可以看出来,context
是函数指针,所以NULL
更符合语境。
但是!context
在一个观察者
观察多个被观察者
的时候,如果多个被观察者
的属性名称
或者说属性路径
也就是keyPath
是相同的时候,会更方便,可以直接利用context
的不同,来分别被观察者
是谁发生了变化。比如 :
创建一个继承与NSObject
的JDPerson
类,再创建一个继承与JDPerson
的JDStudent
类。在ViewController
中给他们的同一个属性name
添加观察者为ViewController
。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self jd_kvo_addObserver];
}
- (void)jd_kvo_addObserver
{
self.person = [[JDPerson alloc] init];
self.student = [[JDStudent alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- 不使用
context
,你需要先判断object
是谁,然后再根据观察的属性做事情。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
//如果是JDPerson的对象的name属性
if ([object isMemberOfClass:[JDPerson class]]) {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"这是对self.person对象的name属性发生变化做事情");
}
}
//如果是JDStudent的对象的name属性
if ([object isMemberOfClass:[JDStudent class]]) {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"这是对self.student对象的name属性发生变化做事情");
}
}
}
- 使用
context
,你只需要通过context
就可以判断被观察者
是谁,发生变化的同名属性属于哪个被观察者
。
先在ViewController
也就是观察者
的上面添加两个全局的静态函数指针。
#import "ViewController.h"
#import "JDPerson.h"
#import "JDStudent.h"
static void* JDPersonNameContext = &JDPersonNameContext;
static void* JDStudentNameContext = &JDStudentNameContext;
@interface ViewController ()
@property (nonatomic, strong) JDPerson *person;
@property (nonatomic, strong) JDStudent *student;
@end
然后在观察回调方法
中的判断就可以变为 :
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
//直接就可以用context进行判断
if (context == JDPersonNameContext) {
NSLog(@"这是对self.person对象的name属性发生变化做事情");
}
else if (context == JDStudentNameContext) {
NSLog(@"这是对self.student对象的name属性发生变化做事情");
}
else {
//所有不被识别的context都必须归属到super调用该方法
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
结论 :
context
的合理利用,比如用于不同被观察者
拥有相同的keyPath
,可以提高代码的可读性,减少代码的复杂度,提高性能。
三、KVO通知的规则
1. 兼容
为了确保被观察的特定属性
是符合KVO
机制的,特定属性
必须满足以下内容 :
这里说一下,里面所有说的该类都是指 —— 被观察的特定属性所属的类
。
- 该类必须符合
KVC
的规定。而且KVO
支持与KVC
相同的数据类型,包括OC对象
以及Scalar
和Structure Support
列表中支持的标量和结构。
- 该类会为属性发出
KVO
中的更改通知。
- 存在依赖关系的
Keys
要适当的注册KVO
,因为存在依赖关系,所以影响很多。
2. 自动发送KVO
通知
在开始的时候介绍说不可以手动的发送通知,其实说的不是很严谨,我们的确。
在默认情况下,遵循KVO
机制的类中都有如下一个方法 :
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
作用 :
-
return YES;
:这是默认的情况。如果是这种情况,那么无论何时,只要接收到了对这个key
做操作的KVC
消息,或者调用了key
的兼容KVC
机制的可变方法,类就会自动调用
以下方法 :-
-willChangeValueForKey:/-didChangeValueForKey:
(简单类型的属性用) -
-willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey:
(数组类型的属性用) -
-willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects:
(集合类型的属性用)
-
-
return NO;
:就不会自动调用上述方法,类就不会发送KVO
通知。
3. 手动发送KVO
通知
这个就是+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
在某个被观察的类中,被我们写成了return NO;
的情况。
如果想要发送通知就要实现2. 自动发送KVO
通知中return YES
调用的几个方法,也就是 :
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
还有另外四个,分别对应了数组类型和集合类型。
4. 属性依赖
在JDPerson
中创建属性
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
其中downloadProgress
下载进度是会根据writtenData
写入数据和totalData
总数据量来决定的,关系是downloadProgress
= writtenData
/ totalData
。
那么在给downloadProgress
添加了观察者ViewController
以后,downloadProgress
的主要变化还是要看writtenData
和totalData
怎么变。
在JDPerson.m
中实现downloadProgress
的set
方法 :
- (NSString *)downloadProgress
{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f * self.writtenData/self.totalData];
}
并在这里实现影响downloadProgress
属性的两个属性,这是系统方法 :
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
return [NSSet setWithObjects:@"totalData", @"writtenData",nil];
}
这样就可以达到downloadProgress
添加了观察者之后,数值是随着totalData
和writtenData
的变换,按照downloadProgress
= writtenData
/ totalData
来进行变化了。
5. 可变数组
这里就要清楚的明白一点,也是一直强调的一点,KVO
是基于KVC
存在的,所以想要使用KVO
观察可变数组,那么可变数组的变化必须是通过KVC
形式进行的。
在JDPerson
类中添加可变数组的属性 :
@property (nonatomic, strong) NSMutableArray *dateArray;
在观察者ViewController
中添加对它初始化,并且添加观察 :
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
这里我们直接使用ViewController
的touchBegin
来让dateArray
添加元素,然后利用KVO
的观察回调方法observeValueForKeyPath
来观察变化,
touchBegin
:
//这里要使用KVC的方法获取dateArray,不能直接使用属性的.方法的setter
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
结果 :
![](https://img.haomeiwen.com/i16702189/353e6ccc28b0175b.png)
这里可以看到一个kind
,这个kind
会和上面的简单类型
的属性不一样,变成了2,简单类型一般都是1。kind
是NSKeyValueChange
类型的枚举,枚举值如下 :
NSKeyValueChange
:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, //设置值
NSKeyValueChangeInsertion = 2, //插入值
NSKeyValueChangeRemoval = 3, //移除值
NSKeyValueChangeReplacement = 4, //替换值
};
对于可变数组和集合,官方文档都是有很详细的书写的,都要用KVC
的设值方式才可以进行观察,更官方的文案在这里。
那么到这里,KVO
的一个最基本,最简单的使用和思路,应该就比较清楚了。普通的使用应该不会有什么问题了,本节就结束,下一节再探索KVO
的一个原理。
网友评论