在 KVO 的生命周期管理中,移除观察者的时机和顺序至关重要。如果处理不当,会直接导致程序崩溃或逻辑异常。
1. 移除 Observer 的正确顺序与时机
核心原则:先解绑,后销毁。
-
最晚时机: 必须在观察者(Observer)或被观察者(Subject)其中任何一方销毁(
dealloc)之前移除。 -
推荐做法:
-
如果在
init或viewDidLoad中添加,通常在dealloc或viewDidUnload(已废弃)中移除。 -
在
dealloc中移除是最后的防线:Objective-C
- (void)dealloc { // 必须在对象内存回收前移除 [self.observedObject removeObserver:self forKeyPath:@"name" context:kMyContext]; }
-
2. 如果移除晚了(对象已经 dealloc),会发生什么?
这是 KVO 最臭名昭著的崩溃来源,具体取决于哪一方先死:
场景 A:观察者(Observer)先 dealloc,但未移除监听
- 后果: EXC_BAD_ACCESS (野指针崩溃) 。
- 原因: KVO 内部通过一个
unsafe_unretained的指针持有观察者地址(为了性能,它不使用weak)。当被观察属性改变时,KVO 尝试向那个已经失效的内存地址发送-observeValueForKeyPath:...消息。此时观察者对象已销毁,地址上可能是乱码或已被其他对象占用,导致直接崩溃。
场景 B:被观察者(Subject)先 dealloc
- 后果: 早期系统会崩溃,现代系统(iOS 10+)会在控制台打印警告,但若管理不当仍有风险。
- 原因: 被观察对象在
dealloc时,如果发现自己身上还挂着监听者,系统会尝试清理。但如果此时监听信息处于不一致状态,或者你试图在dealloc之后再去remove它,会导致找不到 KeyPath 注册表的崩溃。
3. KVO 移除的“地雷区”
除了顺序,移除时还有两个常见的 Crash 模式:
A. “未注册却移除” (Double Remove)
如果你对同一个 KeyPath 连续调用了两次 removeObserver:,或者移除一个从未注册过的 KeyPath。
- 现象:
NSRangeException崩溃,提示Cannot remove an observer for the key path "xxx" because it is not registered as an observer.。 - 防范: 尽量使用
context并在try-catch(不推荐)或通过状态变量来标记是否已注册。
B. “父类/子类移除冲突”
如果父类和子类都监听了同一个属性,且都没有使用 context。
- 现象: 移除时可能移错了对象,或者导致其中一方的移除失败,进而引发后续的野指针崩溃。
- 正确姿势: 始终传入唯一的
context指针,确保“谁添加,谁负责移除”。
4. 终极解决方案:iOS 11+ 的新 API
为了解决上述烦人的生命周期问题,苹果推出了基于闭包的 API,这在 Swift 和现代 OC 中是 首选:
Objective-C
// 此时 self 强持有 observation 对象
self.observation = [target observe:@"name" options:NSKeyValueObservingOptionNew changeHandler:^(id obj, NSDictionary *change) {
// 业务逻辑
}];
- 为什么安全? 这个返回的
NSKeyValueObservation对象会自动管理生命周期。当self销毁时,observation属性随之销毁,它在自己的dealloc中会自动调用解绑逻辑。你不再需要手动写removeObserver了。
总结建议
- 能用新 API 就用新 API(闭包版)。
- 旧版必须配对出现:
add和remove必须在对称的生命周期方法中成对出现。 - 永远携带
context:防止继承链中的误删。 - 防御性解绑:如果不确定对象是否还活着,不要在其他异步线程去跨线程执行移除操作。