iOS 底层原理:KVO (Key-Value Observing)
1. 实现机制:ISA Swizzling
KVO 是通过 Runtime 动态生成子类实现的。
当 [obj addObserver:...] 被调用时:
- 动态子类化:Foundation 通常为原类动态生成一个类似
NSKVONotifying_OriginalClass的子类。这个命名和具体实现属于私有细节,只适合解释现象,不应作为业务逻辑依赖。 - 修改 ISA:将
obj的isa指针指向这个新生成的中间类。 - 重写 Setter:在子类中重写被观察属性的 setter 方法。
2. Setter 重写逻辑
重写后的 setter 伪代码大致如下:
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
// 调用父类(原类)的 setter 实现
// 或者是通过 handle 获取原 setter imp
[super setName:name];
[self didChangeValueForKey:@"name"];
}
didChangeValueForKey 内部会触发 observeValueForKeyPath 回调。
3. 其它覆盖的方法
除了 setter,NSKVONotifying_xxx 类还重写了:
-
class:返回父类的 Class 对象。
- 目的:欺骗开发者。调用 [obj class] 依然返回 OriginalClass,隐藏 KVO 的底层实现。
-
dealloc:参与 KVO 状态清理;具体覆盖集合会随系统实现变化。
-
_isKVOA:一些系统版本中可见的私有标记方法。它不是公开 API,不要在业务代码中调用。
4. 面试题:手动触发 KVO?
可以。
-
关闭自动触发:+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 返回 NO。
-
手动调用:
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
willChange... 与 didChange... 必须成对、正确嵌套。异常路径漏掉 didChange... 会破坏通知状态,因此手写通知时要尽量缩小两者包围的代码范围。
5. 哪些修改能够自动触发
KVO 依赖符合 KVC 约定的变更入口。通过属性 setter 修改值通常可以自动触发;直接写 ivar 会绕过 setter,一般不会自动通知。
self.name = @"new"; // 通常触发
_name = @"new"; // 直接写 ivar,通常不触发自动 KVO
对 to-many 关系,不要直接取出可变数组后随意修改并期待完整的增删语义通知。可以使用 KVC 提供的 mutable proxy,例如:
[[self mutableArrayValueForKey:@"items"] addObject:item];
如果一个计算属性依赖其他 key,可以实现 +keyPathsForValuesAffectingValueForKey:,或为固定关系提供 +keyPathsForValuesAffectingFullName 这一类方法,让依赖属性变化时也产生通知。
6. Observer 管理与 Context
传统 KVO 回调会把多个观察关系汇聚到同一个方法。判断时优先使用唯一 context,而不是只比较字符串 keyPath:
static void *UserNameContext = &UserNameContext;
[user addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionInitial |
NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld
context:UserNameContext];
回调中先判断 context;不是自己处理的观察关系,应交给 super。观察注册和移除要保持对称。较新的系统 API 与 Swift 的 observation token 能降低管理成本,但仍要理解 token 由谁持有、何时失效。
7. 常见误区
- KVO 不是 Method Swizzling 的同义词:两者都利用 Runtime 动态能力,但经典 KVO 的关键是动态子类化和 isa 指向变化。
object_getClass(obj)与[obj class]结果可能不同:前者能看到对象实际 isa 指向的动态子类,后者通常被覆写成返回原类。- 不是所有属性都天然 KVO compliant:自定义访问器、直接 ivar 写入、集合内部变化、非 KVC 兼容模型都需要单独判断。
- 不要依赖私有类名和私有方法:理解它们是为了建立模型,不是为了在线上代码里检测或操纵 KVO。