6-12.【OC】【KVC/KVO】KVO 移除 observer 的正确顺序是什么?如果移除晚了或者对象已经 dealloc,会发生什么?

4 阅读3分钟

在 KVO 的生命周期管理中,移除观察者的时机和顺序至关重要。如果处理不当,会直接导致程序崩溃或逻辑异常。

1. 移除 Observer 的正确顺序与时机

核心原则:先解绑,后销毁。

  • 最晚时机: 必须在观察者(Observer)或被观察者(Subject)其中任何一方销毁(dealloc)之前移除。

  • 推荐做法:

    • 如果在 initviewDidLoad 中添加,通常在 deallocviewDidUnload(已废弃)中移除。

    • 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 了。

总结建议

  1. 能用新 API 就用新 API(闭包版)。
  2. 旧版必须配对出现addremove 必须在对称的生命周期方法中成对出现。
  3. 永远携带 context:防止继承链中的误删。
  4. 防御性解绑:如果不确定对象是否还活着,不要在其他异步线程去跨线程执行移除操作。