一、KVO简介
KVO
是Objective-C
对观察者模式(Observer Pattern)
的实现,也是Cocoa Binding
的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。
二、KVO的基本使用
- 通过
addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keyPath
属性的变化事件
/*
@observer:观察者
@keyPath:想要观察的对象属性
@options:options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样当属性值发生改变时我们可以同时获得旧值和新值,如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值
@context:想要携带的其他信息
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- 在观察者中实现
observeValueForKeyPath:ofObject:change:context:
方法,当keyPath
属性发生改变后,KVO
会回调这个方法来通知观察者
/*
@keyPath:观察的属性
@object:观察的是哪个对象的属性
@change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
@context:添加观察者时携带的信息
*/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- 当观察者不需要监听时,可以调用
removeObserver:forKeyPath:
方法将KVO
移除。注意调用removeObserver
需要在观察者消失之前,否则会导致Crash
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
三、KVO实现机制
KVO
是通过isa
混写(isa-swizzling)
技术实现。
- 当观察一个对象时,一个新的类会动态被创建(动态添加的类名就是在原来类的类名前加上
NSKVONotifying_类名
); - 这个类继承自该对象原本的类,并重写被观察属性的
setter
方法 - 重写的
setter
方法会负责在调用原setter
方法之前和之后,通知所有观察对象值的更改 - 最后把这个对象的
isa
指针 (isa
指针告诉Runtime
系统这个对象的类是什么 ) 指向这个新创建的子类,对象就变成了新创建的子类的实例。
四、KVO的自动触发与手动触发
KVO观察的开启和关闭有两种方式,自动
和手动
自动开关,返回NO
,就监听不到,返回YES
,表示监听;对于想要手动通知的属性,可以根据它的keyPath
返回NO
,而其对于其他位置的keyPath
,要返回父类的这个方法。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要实现手动通知,你需要在值改变前调用 willChangeValueForKey:
方法,在值改变后调用 didChangeValueForKey:
方法。你可以在发送通知前检查值是否改变,如果没有改变就不发送通知。
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != _openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}
使用手动开关的好处就是你监听就监听,不想监听关闭即可,比自动触发更方便灵活
如果一个操作会导致多个属性改变,你需要嵌套通知,像下面这样:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
在一个一对多的关系中,你必须注意不仅仅是这个key
改变了,还有它改变的类型以及索引。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
}
五、KVO一对多,键值依赖
通过注册一个KVO观察者,可以监听多个属性的变化
比如目前有一个需求,需要根据总的下载量totalData
和当前下载量currentData
来计算当前的下载进度currentProcess
,实现有两种方式
- 分别观察
totalData
和currentData
两个属性,当其中一个发生变化计算currentProcess
- 实现
keyPathsForValuesAffectingValueForKey
方法,将两个观察合为一个观察,即观察当前下载进度currentProcess
//1、合二为一的观察方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"currentProcess"]) {
NSArray *affectingKeys = @[@"totalData", @"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
//2、注册KVO观察
[self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];
//3、触发属性值变化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.currentData += 10;
self.person.totalData += 1;
}
//4、移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"currentProcess"];
}
你也可以通过实现 keyPathsForValuesAffecting<Key>
方法来达到前面同样的效果,这里的<Key>
就是属性名,不过第一个字母要大写,用前面的例子来说就是这样:
+ (NSSet *)keyPathsForValuesAffectingCurrentProcess {
return [NSSet setWithObjects:@"totalData", @"currentData", nil];
}
六、KVO观察 可变数组
KVO是基于KVC基础之上的,所以可变数组如果直接添加数据,是不会调用setter
方法的,即直接通过[self.person.dateArray addObject:@"1"];
向数组添加元素,是不会触发KVO通知回调的
在KVC官方文档中,针对可变数组的集合类型,有如下说明,即访问集合对象需要需要通过
mutableArrayValueForKey
方法,这样才能将元素添加到可变数组中;
// KVC 用此方法添加则可以触发KVO
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
七、其他知识点
- 通过
KVC
修改属性会触发KVO
,KVC
内部做了监听操作 - 直接修改成员变量不会触发
KVO
,没走set方法 - 哪些情况下使用
kvo
会崩溃,怎么防护崩溃?
1.
removeObserver
一个未注册的keyPath
,导致错误:Cannot remove an observer A for the key path "str",because it is not registered as an observer
.
解决办法:根据实际情况,增加一个添加keyPath的标记,在dealloc中根据这个标记,删除观察者。
2.添加的观察者已经销毁,但是并未移除这个观察者,当下次这个观察的keyPath
发生变化时,kvo
中的观察者的引用变成了野指针,导致crash
。
解决办法:在观察者即将销毁的时候,先移除这个观察者。
其实还可以将观察者observer
委托给另一个类去完成,这个类弱引用被观察者,当这个类销毁的时候,移除观察者对象
- kvo的优缺点
优点:
1.能够提供一种简单的方法实现两个对象间的同步
2.能够对非我们创建的对象,即内部对象的状态改变做出响应,而且不需要改变内部对象的实现
3.能够提供观察的属性的最新值以及先前值
4.用key paths来观察属性,因此也可以观察嵌套对象
5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
缺点:
1.我们观察的属性必须使用string来定义,因此在编译期不会出现警告以及检查
2.对属性重构将导致我们的观察代码不再可用
3.只能通过重写 -observeValueForKeyPath:ofObject:change:context:
方法来获得通知。
4.不能通过指定selector
的方式获取通知。
5.不能通过block
的方式获取通知。
- 添加观察者和移除观察者要相对应;
- 不要将已经释放的观察者对象,再进行移除;
- 可以多次对同一个属性添加相同的观察者,当属性更改的时候,会多次调用接收方法,不过移除观察者也要执行多次;
- 在iOS10及其以下,不移除观察者会出现闪退的情况,在iOS11及其以上,不会出现闪退的情况;
网友评论