小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
KVO的定义
iOS开发中,KVO模式会经常被用到,通过监听对象的属性值变化进而处理一些业务逻辑,那KVO是如何去监听的?属性值的变化是如何通知监听者的?首先通过苹果的官方文档Key-Value Observing Programming Guide对KVO的定义。
Key-value observing
提供了一种机制,允许在其他对象的特定属性发生变化时通知对象。
KVO流程图
根据上面的定义,可以简单的画个KVO的流程图
KVO的使用
首先我们看一下KVO的代码使用,直接调用相关API,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[ATPerson alloc] init];
[self.person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:NULL];
}
代码很简单,直接在viewDidLoad
方法里添加addObserver
方法,对person
对象的name
属性进行监听,我们平时Context
都是传的NULL
,这里Context
有什么作用?
KVO中的Context
首先看一下官方文档对Context
的定义。
在
addObserver:forKeyPath:options:context:
回调函数返回数据中,观察者将接收到包含任意的数据。你可以指定NULL并完全依赖keyPath
来确定更改通知的来源,但是这种方法可能会导致对象的问题,因为该对象的超类也因为不同的原因观察相同的keyPath
。 更安全、更可扩展的方法是使用上下文来确保接收到的通知是发给观察者的,而不是超类。 当在一个类中添加多个KVO监听时,因此我们可以指定Context
就可以确定监听属性的来源,避免在同一个回调方法里做过多的判断逻辑。
示例代码
static void *PersonHobbyContext = &PersonHobbyContext;
static void *PersonNameContext = &PersonNameContext;
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[ATPerson alloc] init];
[self.person addObserver:self
forKeyPath:@"hobby"
options:NSKeyValueObservingOptionNew
context:PersonHobbyContext];
[self.person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:PersonNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if (context == PersonHobbyContext) {
// hobby
} else if (context == PersonNameContext) {
// name
} else {
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
通过上面指定Context
,我们可以在同一个对象监听不同的属性值变化,而当属性值变化时就不需要通过多层判断来确定,这样可以提高代码的可读性,减少冗余代码。
移除观察者
在类的销毁过程中是否需要移除观察者,还是看一下官方文档关于移除观察者的解释。
- 如果没有注册观察者,移除观察者会导致NSRangeException。当你
addObserver:forKeyPath:options:context:
就要调用removeObserver:forKeyPath:context:
一次,或者如果这在你的应用程序中不可用,把removeObserver:forKeyPath:context:
调用放在try/catch
块中来处理潜在的异常。- 当被释放时,观察者不会自动移除自己。被观察的对象继续发送通知,无视观察者的状态。然而,与任何其他消息一样,发送到已释放对象的更改通知会触发内存访问异常。因此,你可以确保观察者在从内存释放之前将自己移除 下面通过一个示例来验证一下不移除监听会有什么问题。
移除监听案例分析
代码部分
首页HomeVC
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"首页";
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"下一页"
style:UIBarButtonItemStylePlain
target:self
action:@selector(nextClick:)];
}
- (void)nextClick:(UIBarButtonItem *)item {
[self.navigationController pushViewController:[[SecondVC alloc] init] animated:YES];
}
SecondVC
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"KVO";
// 这里通过单例的方式来验证
self.person = [ATPerson sharedInstane];
[self.person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
NSLog(@"SecondVC -- %@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = @"hello world";
}
操作步骤
- 在首页点击
下一页
push到SecondVC
- 点击屏幕触发监听回调
- 返回首页,再次进入
- 点击屏幕再次触发监听回调
运行,第一次进到
SecondVC
页面点击屏幕正常,当退回到HomeVC
再次点击进入SecondVC
点击屏幕程序Crash
。
crash原因分析
造成第二次Crash
的原因,是由于我们对person
设置了单例,第一次进来正常监听回调,当pop
出来时,原来的SecondVC
内存已释放,第二次再次进入开辟了新的内存空间给SecondVC
,但由于我们没有在dealloc
时移除监听,person
还是会给原来的SecondVC
发送监听消息,访问原来已释放的地址造成Crash
。虽然造成这种Crash
并一定发生,但为了代码的强壮型,注意加上移除监听操作。
注意
:不管什么情况,如果给对象添加了KVO监听,在对象销毁时一定要记得移除监听。
手动观察
KVO也可以通过手动控制监听对应的属性,需要关闭自动监听函数,默认是开启的。
手动控制
自动监听函数,需要设置返回NO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
当需要监听指定属性时,需要在监听的对象中重新setter方法,如下:
代码示例
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
在函数automaticallyNotifiesObserversForKey
返回NO
时,KVO
只会监听自定义实现的setter
方法,其他的属性值会忽略。
KVO的一对多观察
当观察对象的属性值之间存在相关联时,对于这种情况需要实现KVO
的keyPathsForValuesAffectingValueForKey
函数,重写KeyPaths
关联的属性值。
当一个属性的getter方法使用其他属性(包括由键路径定位的属性)的值计算要返回的值时,可以重写此方法。您的覆盖通常应该调用super并返回一个集合,该集合中包含这样做的结果中的任何成员(以便不干扰超类中此方法的覆盖)。 下面通过一个简单的下载示例来演示。
示例代码
在ATPerson
类里添加3个属性,当前下载进度 = 已下载 / 总数据
,在点击屏幕时改变对应的属性值,KVO
监听回调里打印相关信息。
ATPerson.h
@interface ATPerson : NSObject
@property (nonatomic, copy) NSString *downloadProgress; // 当前下载进度
@property (nonatomic, assign) double writtenData; // 已下载
@property (nonatomic, assign) double totalData; // 总数据
@end
ATPerson.m
// 重写KVO监听的属性值
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (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];
}
在HomeVC
的touchBegin
事件中改变属性值的变化。
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[ATPerson alloc] init];
[self.person addObserver:self
forKeyPath:@"downloadProgress"
options:NSKeyValueObservingOptionNew
context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.writtenData += 10;
self.person.totalData += 1;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
NSLog(@"%@", change);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
}
运行代码,在HomeVC
点击屏幕,查看日志,可以看到没点击一次都可以监听到下载进度的变化。
对可变数组的观察
接下来我们对ATPerson
添加一个可变数组属性dataArray
,然后在HomeVC
中对这个属性进行观察。
示例代码
HomeVC.m
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[ATPerson alloc] init];
[self.person addObserver:self
forKeyPath:@"dateArray"
options:NSKeyValueObservingOptionNew
context:NULL];
self.person.dateArray = [[NSMutableArray alloc] init];
[self.person.dateArray addObject:@"hello"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
NSLog(@"%@", change);
}
运行,看打印信息可变数组dataArray
没有添加任何信息。
分析
根据之前的KVO
属性监听方式,都是采用一样的方式,但实际监听可变数组
时发现结果却没有改变新值,带着疑问查看苹果关于KVO的官方文档,在文档介绍中首先就有下面这个提示。
重要:要理解KVO,必须首先KVC. 所以再去找到KVC的文档,对于集合类型的键值观察提供是下面几个方法。
1. mutableArrayValueForKey: 和 mutableArrayValueForKeyPath:
2. mutableSetValueForKey: 和 mutableSetValueForKeyPath:
3. mutableOrderedSetValueForKey: 和 mutableOrderedSetValueForKeyPath:
接下来对可变数组采用这个方式来进行监听,代码如下,再次运行:
可以看到监听到了可变数组的属性值变化。
总结
从官方文档对KVO的定义,采用实际案例代码对KVO使用的验证,KVO键值观察是结合KVC的使用方式,对KVO属性观察需要参照KVC的实现原理,通过上面的分析,做以下几点总结:
- 我们可以对一个对象的属性进行监听,常规的属性直接调用
addObserver
,特殊的属性需要参照KVC中的定义,如集合类。 - 在对象销毁时注意要移除监听
- 可以设置手动的监听方式
- 如果一个属性依赖多个属性值,需要重写
keyPathsForValuesAffectingValueForKey
方法。 以上就是本篇对KVO的使用的基本介绍,关于KVO原理分析将通过下一篇进行探索。