本文为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"];
结果 :
图3.5.0.png
这里可以看到一个kind,这个kind会和上面的简单类型的属性不一样,变成了2,简单类型一般都是1。kind是NSKeyValueChange类型的枚举,枚举值如下 :
NSKeyValueChange :
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, //设置值
NSKeyValueChangeInsertion = 2, //插入值
NSKeyValueChangeRemoval = 3, //移除值
NSKeyValueChangeReplacement = 4, //替换值
};
对于可变数组和集合,官方文档都是有很详细的书写的,都要用KVC的设值方式才可以进行观察,更官方的文案在这里。
那么到这里,KVO的一个最基本,最简单的使用和思路,应该就比较清楚了。普通的使用应该不会有什么问题了,本节就结束,下一节再探索KVO的一个原理。











网友评论