6-10.【OC】【KVC/KVO】为什么在多层继承下,KVO 的通知行为可能出乎意料?

2 阅读3分钟

在多层继承(Subclassing Hierarchy)下,KVO 的行为之所以会让你感到“出乎意料”,主要源于其底层的 isa-swizzling 机制Objective-C 消息转发路径之间的冲突。

以下是导致通知行为异常或重复的三个核心根源:


1. 观察者冲突:父类与子类监听同一个 Key

这是最常见的意外场景。如果父类 Class A 监听了 self.name,而子类 Class B 也监听了 self.name

  • 意外表现:name 改变时,同一个观察者方法 observeValueForKeyPath:ofObject:change:context: 会被触发多次,或者子类的实现意外覆盖了父类的逻辑。
  • 底层根源: KVO 的回调分发是基于方法名的。如果你在子类中重写了观察者回调但没有调用 [super observeValue...],父类的监听逻辑就会彻底失效。
  • 解决方案: 始终在注册时传入唯一的 context 指针(通常是当前类定义的静态变量地址),并在回调中通过 context 判断是谁的监听。

2. 动态子类的“多重覆盖”

KVO 的核心是创建一个 NSKVONotifying_DerivedClass

  • 意外表现: 如果你在多层继承中对同一个属性进行复杂的 Setter 重写,KVO 可能无法正确“钩住”最底层的实现。

  • 底层原理: 1. KVO 会寻找当前实例所属类的 Setter 方法。

    1. 它会动态生成一个子类并重写该 Setter,在其中调用 [super setXxx:]

    2. 如果你的继承链条中,父类和子类都对同一个 Setter 有复杂的逻辑处理,KVO 插入的 willChange/didChange 可能会穿插在错误的逻辑节点之间。


3. 手动触发与自动触发的叠加

父类可能设置了 automaticallyNotifiesObserversForKey:NO(手动触发),而子类可能没有意识到这一点。

  • 意外表现: 通知可能会丢失,或者在某些操作下收到两次通知(一次自动,一次手动)。

  • 逻辑风险: * 如果父类决定手动控制通知,它会在其 Setter 里写 willChangeValueForKey:

    • 如果子类继承后直接使用了 KVC 或通过其他方式修改,而没有遵循父类的手动触发规范,观察者将永远收不到通知。

4. 路径依赖断裂:keyPathsForValuesAffectingValueForKey

在多层继承中,我们常用这个方法来让一个“组合属性”依赖于其他属性的变化。

  • 意外表现: 子类增加了一个依赖项,结果父类的依赖项失效了,或者导致了递归触发。
  • 根源: 如果子类重写了该方法但没有通过 [super keyPaths...] 合并父类的集合,那么父类定义的依赖关系将全部丢失。

Objective-C

// 错误示范:会导致父类的依赖关系丢失
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObject:@"lastName"];
}

// 正确示范:合并父类集合
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        keyPaths = [keyPaths setByAddingObject:@"lastName"];
    }
    return keyPaths;
}

总结:多层继承下的 KVO 守则

风险点表现防御方案
回调污染父类/子类逻辑混淆必须使用 context 参数
通知丢失重写了回调却没调 super必须调用 [super observeValue...]
依赖丢失组合属性监听失效必须合并 keyPathsForValuesAffecting 集合
类型混淆isa 动态改变导致调试困难使用 object_getClass 确认真实类型