在多层继承(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方法。-
它会动态生成一个子类并重写该
Setter,在其中调用[super setXxx:]。 -
如果你的继承链条中,父类和子类都对同一个
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 确认真实类型。 |