添加带NSKeyValueObservingOptionInitial的KVO观察者时出现异常回调问题
问题场景
我有一个继承自NSObject的UI类Label,对Core Data中继承自NSManagedObject的Region对象的三个属性做KVO观察:
start(float类型)end(float类型)name(NSString类型)
实现代码如下:
-(void) setRegion:(Region *)region { _region = region; [region addObserver:self forKeyPath:@"start" options:NSKeyValueObservingOptionNew context:nil]; [region addObserver:self forKeyPath:@"end" options:NSKeyValueObservingOptionNew context:nil]; [region addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:nil]; // problematic } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"%@", keyPath); }
(已在其他方法中正确移除观察者)
异常现象
执行标记为// problematic的代码行时,observeValueForKeyPath:被调用3次,分别对应start、end、name三个键,但该行仅注册了name的观察,本应只触发一次回调。即使给每个观察设置独立的context参数,问题依然存在。
但以下两种操作可避免问题:
- 将
name的观察代码移到方法开头 - 给
start和end的观察也添加NSKeyValueObservingOptionInitial选项
另外,Region是一个抽象类的子类(抽象类定义了start、end、name属性),如果观察另一个同抽象父类的具体子类对象,则不会出现此问题,且两个子类均未对属性做特殊处理(使用默认访问器,无自定义setter)。
问题原因
这是Core Data针对抽象实体子类的KVO注册逻辑导致的:当部分属性先注册不带NSKeyValueObservingOptionInitial的观察,最后一个带Initial选项的观察被添加时,Core Data内部的元数据处理会触发所有已注册观察属性的初始值通知。抽象类的属性定义在子类实例上的KVO注册被批量关联,导致最后一次带Initial的注册操作触发了所有已观察属性的回调。
解决方法
方法1:统一观察选项
给所有属性的观察都添加NSKeyValueObservingOptionInitial选项,确保每个属性的注册逻辑独立,避免批量触发:
-(void) setRegion:(Region *)region { _region = region; [region addObserver:self forKeyPath:@"start" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:nil]; [region addObserver:self forKeyPath:@"end" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:nil]; [region addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:nil]; }
方法2:调整观察添加顺序
把带NSKeyValueObservingOptionInitial的观察放在最前面添加,避免触发批量通知逻辑:
-(void) setRegion:(Region *)region { _region = region; // 先添加带Initial选项的观察 [region addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:nil]; [region addObserver:self forKeyPath:@"start" options:NSKeyValueObservingOptionNew context:nil]; [region addObserver:self forKeyPath:@"end" options:NSKeyValueObservingOptionNew context:nil]; }
方法3:移除Initial选项(如果不需要初始值)
如果业务上不需要获取属性的初始值,直接移除NSKeyValueObservingOptionInitial,仅监听属性变化:
-(void) setRegion:(Region *)region { _region = region; [region addObserver:self forKeyPath:@"start" options:NSKeyValueObservingOptionNew context:nil]; [region addObserver:self forKeyPath:@"end" options:NSKeyValueObservingOptionNew context:nil]; [region addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; }
验证说明
以上三种方法均能解决抽象实体子类的KVO异常触发问题,而非抽象实体子类无此问题,是因为Core Data对非抽象实体的KVO注册逻辑更直接,不会触发抽象类属性的批量关联通知。
内容的提问来源于stack exchange,提问作者jeanlain

