一、KVO 的本质:动态子类与消息转发
-
动态生成子类
- 当对象首次被添加 KVO 监听时,Runtime 会动态创建名为
NSKVONotifying_ClassName
的子类(如NSKVONotifying_JCAnimal
)。 - 修改对象的
isa
指针,使其指向该子类(并非修改对象的类,而是重定向方法调用路径)。
- 当对象首次被添加 KVO 监听时,Runtime 会动态创建名为
-
重写关键方法
- 子类重写被监听属性的
setter
方法,插入willChangeValueForKey:
和didChangeValueForKey:
调用。 - 子类还重写
class
方法以隐藏自身存在(返回原始类名),避免外部感知。
objc// 伪代码:动态子类 NSKVONotifying_JCAnimal 的实现 - (void)setAge:(int)age { [self willChangeValueForKey:@"age"]; [super setAge:age]; // 调用原始类的 setter [self didChangeValueForKey:@"age"]; } - (Class)class { return JCAnimal.class; // 伪装成原始类 }
- 子类重写被监听属性的
-
通知触发链路
graph LR A[修改属性值] --> B[调用子类重写的 setter] B --> C[willChangeValueForKey] C --> D[原始类 setter 方法] D --> E[didChangeValueForKey] E --> F[通知所有观察者]
二、KVO 的触发条件与限制
-
自动触发场景
- 通过 Setter 方法 修改属性(如
obj.age = 20
)。 - 通过 KVC 修改属性(如
[obj setValue:@20 forKey:@"age"]
)。
- 通过 Setter 方法 修改属性(如
-
无法触发的情况
- 直接修改成员变量 :如
obj->_age = 20
(未调用 setter)。 - 未遵循 KVC 规范 :例如属性未声明
@dynamic
或未实现访问器方法。
- 直接修改成员变量 :如
-
手动触发技巧
objc// 手动通知属性变化(即使直接修改成员变量) [obj willChangeValueForKey:@"age"]; obj->_age = 20; [obj didChangeValueForKey:@"age"];
三、KVC 的底层行为与 KVO 联动
1. KVC 赋值流程 (setValue:forKey:
)
graph TD
A[调用 setValue:forKey:] --> B{是否存在 setKey: 或 _setKey: 方法?}
B -->|是| C[调用对应方法]
B -->|否| D[检查 +accessInstanceVariablesDirectly]
D -->|YES| E[按顺序查找成员变量 _key, _isKey, key, isKey]
E --> F[找到则赋值]
D -->|NO| G[抛出 NSUnknownKeyException]
2. KVC 触发 KVO 的原因
- KVC 内部默认调用属性的 Setter 方法(若存在),从而触发 KVO。
- 若直接赋值成员变量,需依赖
accessInstanceVariablesDirectly
返回YES
,但此时 不会自动触发 KVO ,需手动调用will/didChange
。
四、关键面试题深度解析
Q1:KVO 如何实现属性监听?
- 动态子类:Runtime 生成子类并重写 setter,插入通知逻辑。
- 消息转发 :修改
isa
指针,使方法调用指向子类。 - 通知链路 :通过
didChangeValueForKey:
触发观察者回调。
Q2:直接修改成员变量会触发 KVO 吗?
- 不会 。KVO 依赖 setter 方法或手动触发
willChangeValueForKey:
和didChangeValueForKey:
,直接修改变量绕过了这些路径。
Q3:如何手动触发 KVO?
- 显式调用 :在修改变量前后添加
willChangeValueForKey:
和didChangeValueForKey:
。
Q4:KVC 修改属性会触发 KVO 吗?
- 会。KVC 默认调用 setter 方法,与直接使用 setter 效果相同。
Q5:KVC 的赋值过程是怎样的?
- 方法优先 :查找
setKey:
或_setKey:
方法。 - 成员变量次之 :若允许访问变量,按
_key
→_isKey
→key
→isKey
顺序查找。
五、实战技巧与陷阱规避
-
自动与手动模式切换
-
重写
+automaticallyNotifiesObserversForKey:
控制是否自动通知:objc+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@"age"]) { return NO; // 关闭 age 属性的自动通知 } return [super automaticallyNotifiesObserversForKey:key]; }
-
-
避免野指针崩溃
- 移除观察者前检查 :确保
removeObserver:forKeyPath:
调用次数不超过添加次数。 - 使用关联对象管理观察者(Swift 中推荐闭包 API 自动管理生命周期)。
- 移除观察者前检查 :确保
-
多线程安全
-
KVO 通知在属性修改的线程触发,需在主线程更新 UI 时手动派发:
objcdispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });
-
六、KVO 与替代方案对比
方案 | 优势 | 劣势 | 适用场景 |
---|---|---|---|
KVO | 自动监听、跨组件解耦 | 需手动移除、字符串 KeyPath 易出错 | 数据模型与 UI 同步 |
Delegate | 类型安全、一对一精准通知 | 需定义协议、代码冗余 | 父子组件定制化交互 |
Notification | 全局广播、一对多监听 | 数据传递类型受限(无强类型) | 系统事件(如键盘弹出) |
Combine | 链式处理、线程调度、类型安全 | 仅限 Swift、学习成本高 | 复杂数据流响应式处理 |
七、总结
KVO 的核心价值在于其 隐式监听能力 ,通过 Runtime 动态派发实现无侵入式观察。理解其底层机制(如动态子类、方法重写)有助于规避内存泄漏和多线程问题。在 Swift 中,优先使用 NSKeyValueObservation
的闭包 API 简化生命周期管理,而在需要精细控制时(如性能敏感场景),可结合手动触发模式优化通知频率。