iOS Objective-C KVO 常见用法
前言
KVO
即Key-Value Observing
是苹果提供给开发者的一套键值观察的API,在我们日常开发中经常用到KVO
进行属性的观察,接下来我们将通过该篇文章对KVO
的常见用法进行总结。
1. 观察属性的变化
1.1 基本的属性观察代码
我们最简单的使用KVO就是观察属性的变化,在本例中,我们观察person
对象的name
的改变。
添加观察代码:
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
监听观察代码:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"%@ - %@",self, change);
}
移除代码:
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
}
1.2 一些问题
Question?
在上面我们是观察person
对象name
属性的变化,如果我们同样有一个新的student
对象也有个name
属性,那么我们在监听代码处该如何判断是那个对象的name
呢?
Answer:
- 首先我们可以通过对
object
的判断是否是要观察的对象,但是这样写代码就显得很low,并且需要观察的对象如果太多了,代码看起来很复杂 - 然后我们还可以用
context
,它的定义是(nullable void *)
类型,平常我们基本都不用这个属性,只是传个nil
,其实按照官方的说法则应该传NULL
,其实我们可以通过context
字段来区分不同的对象属性监听。例如:
static void *PersonNameContext = &PersonNameContext;
static void *StudentNameContext = &StudentNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (context == PersonNameContext) {
NSLog(@"person name change %@ - %@",self, change);
} else if (context == StudentNameContext) {
NSLog(@"student name change %@ - %@",self, change);
}
}
2. 嵌套观察
其实在我们的开发中经常会有显示下载进度的需求,下载进度由 totalCount
和completedCount
两个值组成,如果我们观察这两个值就需要在两个地方处理进度的改变,这时候我们希望通过观察一个downloadProgress
就能实现获取下载进度的需求。
2.1 实现代码
相关属性:
// 相关属性
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double completedCount;
@property (nonatomic, assign) double totalCount;
downloadProgress Getter:
- (NSString *)downloadProgress{
if (self.completedCount <= 0) {
self.completedCount = 0;
}
if (self.totalCount <= 0) {
return @"0";
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.completedCount/self.totalCount];
}
keyPathsForValuesAffectingValueForKey:
通过keyPathsForValuesAffectingValueForKey
方法实现多键值的观察。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalCount", @"completedCount"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
添加观察代码:
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
监听观察代码:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"%@ - %@",self, change);
}
当totalCount
和completedCount
任一值改变后都可以触发downloadProgress
的改变,在这里最重要的就是通过+ (NSSet
方法实现一组键值的观察。
3. 观察可变集合
这里我们在KVC
那篇文章中有提到过,对于可变数组的观察需要使用mutableArrayValueForKey
等相关方法。其实主要原因是KVO
观察属性是基于Setter
方法,但是直接调用可变数组的addObject
方法是不走Setter
的,所以此时无法实现可变数组的观察,其他可变集合同理。
添加观察代码:
[self.person addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
监听和移除代码跟上面的类似。这里主要是改变的代码会有不同。
改变数组的代码:
// 数组变化(不会触发改变)
[self.person.dataArray addObject:@"1"];
// 数组变化(会触发改变)
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
打印结果:
通过上图结果我们可以知道,添加@"1"
的时候并不会触发KVO
,在添加@"2"
的时候触发了KVO
的监听改变,所以对于可变集合的KVO
的监听需要用对应的mutableXXXValueForKey
方法进行改变。
4. 自动观察与手动观察
默认情况下,我们只需要按照上面的代码就可以实现属性的观察,其实这是由系统完全控制的,属于自动观察。其实KVO
还给我们提供了手动观察的选项。
如果我们想要开启手动观察就要通过重写类方法+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key
,如果返回YES
就是自动观察,返回NO
就是手动观察,根据方法的我们还可以判断key
值对不同的key
分别实现自动观察和手动观察。
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
} else {
return YES;
}
}
对于需要手动观察的key
在改变前需要调用willChangeValueForKey
方法,在改变后需要调用didChangeValueForKey
方法,如果不调用,就不会触发KVO
的监听。
示例代码:
[self.person willChangeValueForKey:@"name"];
self.person.name = @"null";
[self.person didChangeValueForKey:@"name"];
5. KVO是否需要移除观察
在实验中,对于普通的对象我们不进行移除并没有出现问题,但是对于单例对象就会有问题。这里就不一一验证了,总是就是一句话
一定要移除观察!
一定要移除观察!
一定要移除观察!