1. 什么是KVO
KVO(Key-Value Observing),俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者。
KVO是在MVC应用程序中的各层之间进行通信的一种特别有用的技术。
2. 为什么需要KVO
观察/监听另一个对象指定属性值的改变
3. 怎样使用KVO
提起KVO,相信很多同学都用过。我们可以用KVO监听对象属性值的改变,当属性值发生改变的时候,我们会在监听方法中得到被监听值的改变情况。
我们简单看一下KVO的使用:
#import "ViewController.h"
#import "YZPerson.h"
@interface ViewController ()
@property (strong, nonatomic) YZPerson *p1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
YZPerson *p1 = [[YZPerson alloc] init];
self.p1 = p1;
self.p1.name = @"Jim";
[self.p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"context"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.p1.name = @"sun";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"keyPath = %@, object = %@, change= %@, context= %@", keyPath, object, change, context);
}
/**iOS9以后,可以跟通知一样,不用做监听的移除*/
- (void)dealloc
{
[self.p1 removeObserver:self forKeyPath:@"name"];
}
@end
可以看到打印:
keyPath = name, object = <YZPerson: 0x600003cbc6a0>, change= {
kind = 1;
new = sun;
old = Jim;
}, context= context
接下来,我们来看下KVO的原理
在程序中,加入p2对象。如下:
- (void)viewDidLoad {
[super viewDidLoad];
YZPerson *p1 = [[YZPerson alloc] init];
YZPerson *p2 = [[YZPerson alloc] init];
self.p1 = p1;
self.p2 = p2;
self.p1.name = @"Jim";
self.p2.name = @"Jim2";
[self.p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"context"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.p1.name = @"sun";
self.p2.name = @"sun2";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"keyPath = %@, object = %@, change= %@, context= %@", keyPath, object, change, context);
}
运行结果:
keyPath = name, object = <YZPerson: 0x600003cbc6a0>, change= {
kind = 1;
new = sun;
old = Jim;
}, context= context
运行程序后点击屏幕,可以看到只有p1被监听,而p2并没有被监听。
在点击处打上断点:
可以看到:
p1的isa指针是NSKVONotifying_YZPerson
p2的isa指针是YZPerson
NSKVONotifying_YZPerson为YZPerson的子类。
在p1.name = @"sun";
的setName:方法中调用了_NSSetStringValueAndNotify()
函数方法;_NSSetStringValueAndNotify()
函数中又实现了类似
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
在[self didChangeValueForKey:@"name"];
中实现具体的通知方法。
假如,使用成员变量(非属性)定义name,那么
在代码中使用self.p1->name = @"sun";
不会实现KVO监听的。
因为,这种修改值没有走set方法
同样,我们可以利用
[self.p1 willChangeValueForKey:@"name"];
self.p1 -> name = @"name";
[self.p1 didChangeValueForKey:@"name"];
来手动实现KVO监听。
总结
1.KVO是基于runtime实现的;
2.当某个类的对象第一次被观察时,系统就会在运行期间动态的生成该类的子类,在这个子类中重写基类中任何被观察属性的setter方法。子类在被重写的setter方法实现真正的通知机制(YZPerson->NSKVONotifying_YZPerson);
4. 使用KVO需要哪些注意点?
面试题
1. KVO如果不实现监听方法会怎样?
也就是
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"keyPath = %@, object = %@, change= %@, context= %@", keyPath, object, change, context);
}
方法不写
会崩溃
An -observeValueForKeyPath:ofObject:change:context: message was received but not handled. Key path: name
2. KVO如果多次调用移除操作会怎样?
在dealloc里面多次调用
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"name"];
NSLog(@"%s", __func__);
}
程序崩溃
because it is not registered as an observer
崩溃原因:
当你使用KVO注册观察者后,系统会在内部为这个对象生成一个记录-观察者的列表。每当你调用removeObserver:forKeyPath:
时,系统会试图在这个列表中找到对应的观察者,并从列表中移除它。如果没有找到这个观察者,系统会抛出异常,并导致程序崩溃。
如何解决:
解决这个问题的方案有很多种,下面列出几种常见的方法:
1、在移除观察者前,先检查是否已经添加过:你可以自己在代码中维护一个标记,用来记录观察者是否已经被添加。只有当标记显示观察者已经添加,才执行移除操作。
if (self.isObserving) {
[person removeObserver:self forKeyPath:@"name"];
self.isObserving = NO;
}
2、捕获异常:使用@try/@catch/@finally
语句可以捕获并处理可能抛出的异常,以防止程序崩溃。
@try {
[self.person removeObserver:self forKeyPath:@"name"];
}
@catch (NSException *exception) {
NSLog(@"Exception: %@", exception);
}
@finally {
// Cleanup code here, if necessary.
}
3、使用第三方库:有些第三方库,比如KVOController
,封装了KVO的用法,可以很好地处理这类问题。使用这些库可以大大减少KVO使用中的坑,并且代码也会更加优雅。
无论使用哪种解决方案,对于 KVO 最重要的一点就是要确保每次添加观察者后,都要在适当的时机移除观察者,防止出现内存泄露或无法预计的行为。
更多学习关于KVO崩溃的例子,请看:
iOS 开发:『Crash 防护系统』(二)KVO 防护
3. KVO能否监听对象?
这个题当时把我问蒙了,KVO不是监听属性值的改变吗?咋能去监听对象?对象有set方法调用吗?
最后试了一把,还真能!
#import "YZButton.h"
@interface ViewController ()
@property (strong, nonatomic) YZPerson *person;
@end
@implementation ViewController
//在MRC下重写set方法
- (void)setPerson:(YZPerson *)person
{
if (_person != person) {
[_person release];
_person = [person retain];
}
}
- (void)viewDidLoad {
[super viewDidLoad];
YZPerson *person = [[YZPerson alloc] init];
self.person = person;
person.name = @"sss";
//self的"person"
[self addObserver:self forKeyPath:@"person" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"context"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@ %@ %@", object, change, context);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person = [[NSObject alloc] init];
}
打印结果:
<ViewController: 0x7ffdf3513b00> {
kind = 1;
new = "<NSObject: 0x600000bf8360>";
old = "<YZPerson: 0x6000009bc3c0>";
} context
能调用的原因是,self的person对象,将self.person对象改变,等于也是调用了set方法,因此,可以进行监听。
4. 如果在项目中对Person类进行了监听,也创建了一个NSKVONotifying_Person类,那么会编译通过么?
编译通过,因为KVO是运行时刻创建的,并不在编译时刻,在编译时刻只有一个NSKVONotifying_Person,所以不报错,可以通过
但是此时KVO起不了作用,即监听属性值,KVO监听方法不执行
一运行控制台会有打印:
KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class
5. 为何创建一个新的子类,在子类里面监听属性值的改变?
不改变原有类的内容,在子类中进行修改
-
开闭原则
对修改关闭,对扩展开放 -
里式替换原则
父类可以被子类无缝替换,且原有功能不受任何影响
在KVO监听中,系统重写了setter方法,然后将isa指针指向其子类,悄无声息的子类替换掉父类。
学习文章:
iOS - 关于 KVO 的一些总结