1. KVO 是什么
1.1 概念
KVO(Key-Value Observing)是 Cocoa 提供的一种观察者模式 实现机制,允许对象监听另一个对象特定属性的变化,当该属性值发生变化时,系统会自动通知所有注册的观察者。
KVO 基于 KVC(Key-Value Coding) 构建,属性必须支持 KVC 才能被 KVO 监听。
1.2 观察者模式回顾
erlang
被观察对象 (Subject)
│
│ 属性发生变化
▼
KVO Runtime 机制
│
├──▶ 观察者 A:observeValueForKeyPath:...
├──▶ 观察者 B:observeValueForKeyPath:...
└──▶ 观察者 C:observeValueForKeyPath:...
1.3 KVO vs NSNotification vs Delegate 对比
| 特性 | KVO | NSNotification | Delegate |
|---|---|---|---|
| 监听目标 | 特定对象的特定属性 | 全局通知中心发出的事件 | 特定协议方法 |
| 耦合度 | 低(无需修改被观察类) | 低(需要知道通知名) | 高(需实现协议) |
| 观察者数量 | 多对一 | 多对多 | 一对一 |
| 线程安全 | ⚠️ 通知在属性变更的线程触发 | ⚠️ 通知在 post 的线程触发 | 取决于调用方 |
| 类型安全 | ❌ 运行时字符串 keyPath | ❌ 运行时字符串通知名 | ✅ 编译时检查 |
| 适用场景 | 属性值变化监听 | 系统/全局事件 | 回调单一代理 |
| 崩溃风险 | ⚠️ 忘记移除会崩溃 | ✅ 相对安全 | ✅ 相对安全 |
| Swift 现代写法 | ✅ 有 observe API | ✅ NotificationCenter | ✅ 协议 |
1.4 KVO 的优势场景
- 不想(或不能)修改被观察对象的源码
- 需要同时观察多个属性
- 需要观察第三方框架的属性(如
AVPlayer.status、UIScrollView.contentOffset)
2. 基础用法
2.1 注册观察者
objectivec
// ObjC
[self.person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
php
// Swift(传统方式,兼容 ObjC)
person.addObserver(self,
forKeyPath: "name",
options: [.new, .old],
context: nil)
2.2 参数详解
| 参数 | 类型 | 说明 |
|---|---|---|
observer |
id |
观察者对象,必须实现 observeValueForKeyPath:ofObject:change:context: |
forKeyPath |
NSString * |
要观察的属性路径,支持点语法(如 "address.city") |
options |
NSKeyValueObservingOptions |
控制通知内容(见第 3 节) |
context |
void * |
任意指针,用于标识和区分观察(见第 6 节) |
2.3 实现回调
objectivec
// ObjC
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSString *newName = change[NSKeyValueChangeNewKey];
NSString *oldName = change[NSKeyValueChangeOldKey];
NSLog(@"name changed: %@ -> %@", oldName, newName);
}
}
swift
// Swift(传统方式)
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?) {
if keyPath == "name" {
let newName = change?[.newKey] as? String
let oldName = change?[.oldKey] as? String
print("name changed: (oldName ?? "") -> (newName ?? "")")
}
}
2.4 完整示例(ObjC)
less
// Person.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
// ViewController.m
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.person.name = @"张三";
// 注册观察
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// 触发通知
self.person.name = @"李四"; // 会触发 KVO
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (object == self.person && [keyPath isEqualToString:@"name"]) {
NSLog(@"name: %@ -> %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
}
}
- (void)dealloc {
// ✅ 必须在 dealloc 中移除
[self.person removeObserver:self forKeyPath:@"name"];
}
@end
3. options 详解
NSKeyValueObservingOptions 是一个位掩码枚举,可以用 | 组合使用。
3.1 枚举值说明
| 选项 | ObjC 常量 | Swift 枚举 | 说明 |
|---|---|---|---|
| New | NSKeyValueObservingOptionNew |
.new |
change 字典包含变更后的新值(NSKeyValueChangeNewKey) |
| Old | NSKeyValueObservingOptionOld |
.old |
change 字典包含变更前的旧值(NSKeyValueChangeOldKey) |
| Initial | NSKeyValueObservingOptionInitial |
.initial |
注册时立即触发一次通知(旧值不可用) |
| Prior | NSKeyValueObservingOptionPrior |
.prior |
在值变更之前额外触发一次通知 |
3.2 各选项效果演示
NSKeyValueObservingOptionNew / Old
objectivec
// ObjC
[obj addObserver:self
forKeyPath:@"value"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
// change 字典示例:
// {
// NSKeyValueChangeKindKey: 1 (NSKeyValueChangeSetting),
// NSKeyValueChangeNewKey: @"新值",
// NSKeyValueChangeOldKey: @"旧值"
// }
NSKeyValueObservingOptionInitial
objectivec
// ObjC ------ 注册时会立即触发一次,可用于初始化 UI
[obj addObserver:self
forKeyPath:@"value"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
context:nil];
// ✅ 适合用来初始化 UI 状态,不需要手动调一次 update 方法
php
// Swift
obj.addObserver(self, forKeyPath: "value", options: [.new, .initial], context: nil)
NSKeyValueObservingOptionPrior
css
// ObjC ------ 值变更前后各触发一次
// 变更前的通知:change[NSKeyValueChangeNotificationIsPriorKey] == @YES
[obj addObserver:self
forKeyPath:@"value"
options:NSKeyValueObservingOptionPrior | NSKeyValueObservingOptionNew
context:nil];
objectivec
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
BOOL isPrior = [change[NSKeyValueChangeNotificationIsPriorKey] boolValue];
if (isPrior) {
NSLog(@"即将变更(Prior 通知)");
// 此时值还没变,可以做一些"变更前"的操作
} else {
NSLog(@"已变更,新值: %@", change[NSKeyValueChangeNewKey]);
}
}
3.3 options 为 0 时
objectivec
// ⚠️ options 为 0 时,change 字典里既没有 new 也没有 old,只有 kind
[obj addObserver:self forKeyPath:@"value" options:0 context:nil];
// 此时 change 只包含 NSKeyValueChangeKindKey
4. observeValueForKeyPath 回调解析
4.1 方法签名
objectivec
- (void)observeValueForKeyPath:(NSString *)keyPath // 被观察的属性路径
ofObject:(id)object // 被观察的对象
change:(NSDictionary<NSKeyValueChangeKey, id> *)change // 变更信息字典
context:(void *)context; // 注册时传入的 context
4.2 change 字典的所有 Key
| Key 常量 | Swift 枚举 | 类型 | 说明 |
|---|---|---|---|
NSKeyValueChangeKindKey |
.kindKey |
NSNumber |
变更类型(见下方枚举) |
NSKeyValueChangeNewKey |
.newKey |
id |
新值(需设置 .new option) |
NSKeyValueChangeOldKey |
.oldKey |
id |
旧值(需设置 .old option) |
NSKeyValueChangeIndexesKey |
.indexesKey |
NSIndexSet |
集合变更时的索引(插入/删除/替换) |
NSKeyValueChangeNotificationIsPriorKey |
.notificationIsPriorKey |
NSNumber(BOOL) |
是否为 Prior 通知 |
4.3 NSKeyValueChangeKindKey 枚举
objectivec
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, // 普通属性赋值
NSKeyValueChangeInsertion = 2, // 集合元素插入
NSKeyValueChangeRemoval = 3, // 集合元素删除
NSKeyValueChangeReplacement = 4, // 集合元素替换
};
objectivec
// ObjC 示例
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSKeyValueChange kind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue];
switch (kind) {
case NSKeyValueChangeSetting:
NSLog(@"属性值被设置: %@", change[NSKeyValueChangeNewKey]);
break;
case NSKeyValueChangeInsertion: {
NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
NSArray *newItems = change[NSKeyValueChangeNewKey];
NSLog(@"集合插入,索引: %@,新元素: %@", indexes, newItems);
break;
}
case NSKeyValueChangeRemoval: {
NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
NSArray *oldItems = change[NSKeyValueChangeOldKey];
NSLog(@"集合删除,索引: %@,删除元素: %@", indexes, oldItems);
break;
}
case NSKeyValueChangeReplacement: {
NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
NSLog(@"集合替换,索引: %@", indexes);
break;
}
}
}
swift
// Swift
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?) {
guard let change = change else { return }
let kindRaw = change[.kindKey] as? UInt ?? 0
let kind = NSKeyValueChange(rawValue: kindRaw)
switch kind {
case .setting:
print("设置新值: (change[.newKey] ?? "nil")")
case .insertion:
let indexes = change[.indexesKey] as? IndexSet
print("插入索引: (String(describing: indexes))")
case .removal:
print("删除元素")
case .replacement:
print("替换元素")
default:
break
}
}
4.4 ⚠️ 必须调用 super
objectivec
// ⚠️ 如果不是自己注册的 KVO,必须调 super,否则可能崩溃
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == &MyObservingContext) {
// 处理自己的 KVO
} else {
// ✅ 未知的 KVO,交给父类处理
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
5. 移除观察者
5.1 移除方法
objectivec
// ObjC ------ 指定 keyPath(推荐)
[self.person removeObserver:self forKeyPath:@"name"];
// ObjC ------ 指定 keyPath + context(最精确,推荐用于有继承关系的场景)
[self.person removeObserver:self forKeyPath:@"name" context:&MyObservingContext];
php
// Swift
person.removeObserver(self, forKeyPath: "name")
person.removeObserver(self, forKeyPath: "name", context: &myContext)
5.2 正确的移除时机
objectivec
// ObjC ------ 在 dealloc 中移除
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"age"];
}
php
// Swift ------ 在 deinit 中移除(传统方式)
deinit {
person.removeObserver(self, forKeyPath: "name")
}
5.3 ⚠️ 崩溃场景一览
objectivec
// ⚠️ 场景 1:忘记移除 ------ observer 被释放后 person 变更属性 → EXC_BAD_ACCESS
// 被观察对象持有 observer 的弱引用,observer 释放后成野指针
// ⚠️ 场景 2:重复移除 ------ 会抛出 NSInternalInconsistencyException
[self.person removeObserver:self forKeyPath:@"name"]; // 第一次,正常
[self.person removeObserver:self forKeyPath:@"name"]; // 第二次,崩溃!
// ⚠️ 场景 3:移除从未注册过的 observer
[someObj removeObserver:self forKeyPath:@"xxx"]; // 从未注册过 → 崩溃
5.4 ✅ 安全移除模式
objectivec
// ✅ 方案一:用 @try/@catch 防护(不推荐,治标不治本)
@try {
[self.person removeObserver:self forKeyPath:@"name"];
} @catch (NSException *exception) {
NSLog(@"移除观察者失败: %@", exception);
}
// ✅ 方案二:用 context + 标志位
static void * const kNameContext = &kNameContext;
- (void)startObserving {
if (!_isObserving) {
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kNameContext];
_isObserving = YES;
}
}
- (void)stopObserving {
if (_isObserving) {
[self.person removeObserver:self forKeyPath:@"name" context:kNameContext];
_isObserving = NO;
}
}
- (void)dealloc {
[self stopObserving];
}
javascript
// ✅ 方案三(Swift 推荐):使用 observe API,token 自动管理生命周期
var observation: NSKeyValueObservation?
func startObserving() {
observation = person.observe(.name, options: [.new, .old]) { person, change in
print("name changed: (change.oldValue ?? "") -> (change.newValue ?? "")")
}
}
// observation 置 nil 或 deinit 时自动移除,无需手动 removeObserver
6. context 的妙用
6.1 为什么需要 context
当子类和父类都注册了相同 keyPath 的 KVO 时,observeValueForKeyPath: 无法区分通知来自哪一层。context 就是用来解决这个问题的。
6.2 使用 static void * 作为唯一标识
objectivec
// ✅ ObjC ------ 用静态指针作为 context,每个类唯一
// ParentViewController.m
static void * const kParentObservingContext = (void *)&kParentObservingContext;
@implementation ParentViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.model addObserver:self
forKeyPath:@"value"
options:NSKeyValueObservingOptionNew
context:kParentObservingContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == kParentObservingContext) {
// ✅ 只处理父类注册的 KVO
NSLog(@"ParentVC 收到: %@", change[NSKeyValueChangeNewKey]);
} else {
// ✅ 其他的交给父类
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc {
[self.model removeObserver:self forKeyPath:@"value" context:kParentObservingContext];
}
@end
objectivec
// ChildViewController.m
static void * const kChildObservingContext = (void *)&kChildObservingContext;
@implementation ChildViewController
- (void)viewDidLoad {
[super viewDidLoad]; // 父类也注册了相同 keyPath
[self.model addObserver:self
forKeyPath:@"value"
options:NSKeyValueObservingOptionNew
context:kChildObservingContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == kChildObservingContext) {
// ✅ 只处理子类自己注册的 KVO
NSLog(@"ChildVC 收到: %@", change[NSKeyValueChangeNewKey]);
} else {
// ✅ 其他交给父类处理(父类会处理它自己的 context)
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc {
[self.model removeObserver:self forKeyPath:@"value" context:kChildObservingContext];
}
@end
6.3 为什么用 static void * const 而不是字符串
csharp
// ❌ 不推荐:用字符串作 context,可能与父类字符串内容相同
[obj addObserver:self forKeyPath:@"value" options:0 context:@"myContext"];
// ✅ 推荐:静态指针,地址唯一,永不冲突
static void * const kMyContext = (void *)&kMyContext;
// &kMyContext 是这个静态变量的内存地址,全局唯一
7. KVO 底层原理
7.1 isa-swizzling
KVO 的核心魔法是运行时动态创建子类 并替换 isa 指针,称为 isa-swizzling。
objectivec
注册 KVO 前:
person 对象
┌──────────────┐
│ isa → Person │ ──→ Person 类(正常的 setter)
└──────────────┘
注册 KVO 后:
person 对象
┌────────────────────────────────┐
│ isa → NSKVONotifying_Person │ ──→ 动态子类(重写了 setter)
└────────────────────────────────┘
7.2 动态子类 NSKVONotifying_XXX
当你对 Person 类的对象注册第一个 KVO 时,Runtime 会:
- 动态创建
NSKVONotifying_Person类(Person的子类) - 重写被观察属性的 setter 方法
- 重写
class方法(返回原始的Person,隐藏实现细节) - 重写
dealloc方法(清理 KVO 状态) - 重写
_isKVOA方法(标记为 KVO 子类) - 将对象的
isa指针指向新子类
swift
// 验证代码:注册 KVO 前后查看 isa
Person *p = [[Person alloc] init];
NSLog(@"注册前 class: %@", object_getClass(p)); // Person
NSLog(@"注册前 class方法: %@", [p class]); // Person
[p addObserver:self forKeyPath:@"name" options:0 context:nil];
NSLog(@"注册后 class: %@", object_getClass(p)); // NSKVONotifying_Person
NSLog(@"注册后 class方法: %@", [p class]); // Person(被 KVO 隐藏了)
7.3 重写后的 setter 实现
动态子类重写的 setter 大致等价于:
objectivec
// NSKVONotifying_Person 中 setName: 的伪代码
- (void)setName:(NSString *)name {
// 1. 通知即将变更(Prior 通知)
[self willChangeValueForKey:@"name"];
// 2. 调用原始 setter(通过 super 或直接操作 ivar)
[super setName:name];
// 3. 通知已经变更
[self didChangeValueForKey:@"name"];
}
7.4 完整流程图
css
p.name = @"李四"
│
▼
NSKVONotifying_Person.setName:
│
├─ willChangeValueForKey:@"name"
│ │
│ └─ 遍历观察者列表,发送 Prior 通知(如果设置了 .prior option)
│
├─ [super setName:] ──→ Person.setName: ──→ _name = @"李四"
│
└─ didChangeValueForKey:@"name"
│
└─ 遍历观察者列表,调用每个观察者的 observeValueForKeyPath:...
7.5 ⚠️ 注意事项
ini
// ⚠️ 直接修改 ivar 不会触发 KVO(绕过了 setter)
_name = @"李四"; // ❌ 不触发 KVO
// ✅ 通过 setter 才会触发 KVO
self.name = @"李四"; // ✅ 触发 KVO
// ✅ 或者手动触发(见第 8 节手动 KVO)
[self willChangeValueForKey:@"name"];
_name = @"李四";
[self didChangeValueForKey:@"name"];
8. 手动 KVO
8.1 为什么需要手动 KVO
- 批量更新时,希望只触发一次通知而不是每次 setter 都触发
- 需要对通知时机精确控制
- 直接修改 ivar 但仍需触发通知
- 某个属性不应该通过 KVO 触发(性能优化)
8.2 关闭自动 KVO
objectivec
// ObjC ------ 在被观察类中重写这个方法
@implementation Person
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO; // ✅ 关闭 name 属性的自动 KVO
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
kotlin
// Swift
@objc class Person: NSObject {
@objc dynamic var name: String = ""
// Swift 中关闭自动 KVO
@objc class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
if key == "name" {
return false
}
return super.automaticallyNotifiesObservers(forKey: key)
}
}
8.3 手动触发通知
ini
// ObjC
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
scss
// Swift
@objc var name: String = "" {
willSet { willChangeValue(forKey: "name") }
didSet { didChangeValue(forKey: "name") }
}
8.4 批量变更(减少通知次数)
ini
// ObjC - 用 willChange/didChange 把多次变更合并成一次通知
- (void)updateMultipleProperties {
[self willChangeValueForKey:@"name"];
[self willChangeValueForKey:@"age"];
_name = @"新名字";
_age = 30;
[self didChangeValueForKey:@"age"];
[self didChangeValueForKey:@"name"];
}
9. 集合属性的 KVO
9.1 为什么直接操作数组不会触发 KVO?
less
// ObjC - ❌ 不会触发 KVO
[self.items addObject:newItem]; // 数组本身没变,指针没变
// ✅ 触发 KVO 的正确方式
[[self mutableArrayValueForKey:@"items"] addObject:newItem];
mutableArrayValueForKey: 返回一个代理数组,对它的增删改操作会自动触发 KVO 通知。
9.2 集合变更类型(NSKeyValueChangeKindKey)
| 值 | 说明 |
|---|---|
NSKeyValueChangeSetting |
整体替换(普通属性变更) |
NSKeyValueChangeInsertion |
插入元素 |
NSKeyValueChangeRemoval |
删除元素 |
NSKeyValueChangeReplacement |
替换元素 |
9.3 监听集合变更
objectivec
// ObjC - 被观察者:声明集合属性
@interface DataSource : NSObject
@property (nonatomic, strong) NSMutableArray *items;
@end
// ObjC - 观察者:接收集合变更通知
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSKeyValueChange kind = [change[NSKeyValueChangeKindKey] integerValue];
NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
switch (kind) {
case NSKeyValueChangeInsertion:
NSLog(@"插入了元素,位置: %@", indexes);
break;
case NSKeyValueChangeRemoval:
NSLog(@"删除了元素,位置: %@", indexes);
break;
case NSKeyValueChangeReplacement:
NSLog(@"替换了元素,位置: %@", indexes);
break;
default:
break;
}
}
scss
// ObjC - 触发集合 KVO
DataSource *ds = [[DataSource alloc] init];
[[ds mutableArrayValueForKey:@"items"] addObject:@"item1"]; // 触发 Insertion
[[ds mutableArrayValueForKey:@"items"] removeObjectAtIndex:0]; // 触发 Removal
9.4 自定义集合访问器(更规范的做法)
less
// ObjC - 实现 KVC 集合访问器方法,KVO 自动生效
@implementation DataSource
- (NSUInteger)countOfItems {
return _items.count;
}
- (id)objectInItemsAtIndex:(NSUInteger)index {
return _items[index];
}
- (void)insertObject:(id)object inItemsAtIndex:(NSUInteger)index {
[_items insertObject:object atIndex:index]; // 自动触发 KVO
}
- (void)removeObjectFromItemsAtIndex:(NSUInteger)index {
[_items removeObjectAtIndex:index]; // 自动触发 KVO
}
@end
10. 依赖 Key
当一个属性的值依赖于其他属性时,可以用 keyPathsForValuesAffectingValueForKey: 声明依赖关系,这样观察者在依赖属性变化时也会收到通知。
10.1 基本用法
less
// ObjC - 场景:fullName 依赖 firstName 和 lastName
@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, readonly) NSString *fullName; // 计算属性
@end
@implementation Person
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
// 声明 fullName 依赖 firstName 和 lastName
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}
@end
less
// Swift
@objc class Person: NSObject {
@objc dynamic var firstName: String = ""
@objc dynamic var lastName: String = ""
@objc var fullName: String { "(firstName) (lastName)" }
override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
if key == "fullName" {
return ["firstName", "lastName"]
}
return super.keyPathsForValuesAffectingValue(forKey: key)
}
}
10.2 触发效果
objectivec
// ObjC - 观察 fullName,但修改 firstName 也能触发
[person addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:nil];
person.firstName = @"张"; // ← 触发 fullName 的 KVO 通知
10.3 注意事项
⚠️ 依赖 key 不支持 keyPath(带点号的路径),只支持同一对象的直接属性名。
✅ 推荐用
keyPathsForValuesAffecting<Key>类方法(苹果推荐写法),避免字符串拼写错误。
11. Swift 中的 KVO
11.1 前置条件
Swift 中使用 KVO 有两个硬性要求:
- 类必须继承
NSObject - 被观察属性必须标记为
@objc dynamic
kotlin
// ✅ 正确:继承 NSObject + @objc dynamic
class Counter: NSObject {
@objc dynamic var count: Int = 0
}
// ❌ 错误:struct 不支持 KVO
struct Counter {
var count: Int = 0
}
// ❌ 错误:缺少 @objc dynamic,KVO 不生效(编译不报错,但运行时不会触发)
class Counter: NSObject {
var count: Int = 0
}
11.2 Swift 4+ 新 API(推荐)
typescript
// Swift - 类型安全的 KVO,使用 KeyPath
class ViewController: UIViewController {
let counter = Counter()
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
// observe(_:options:changeHandler:) 返回 NSKeyValueObservation token
observation = counter.observe(.count, options: [.new, .old]) { object, change in
print("count 变了:(change.oldValue ?? 0) → (change.newValue ?? 0)")
}
}
// ✅ observation token 释放时自动移除观察者,无需手动 removeObserver
// deinit 时 observation 被释放,KVO 自动取消
}
11.3 旧 API(ObjC 风格,不推荐在 Swift 中用)
swift
// Swift - 旧式 KVO(不推荐)
class ViewController: UIViewController {
let counter = Counter()
override func viewDidLoad() {
super.viewDidLoad()
counter.addObserver(self, forKeyPath: "count", options: [.new, .old], context: nil)
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?) {
if keyPath == "count" {
print("count changed: (change?[.newKey] ?? "nil")")
}
}
deinit {
counter.removeObserver(self, forKeyPath: "count")
}
}
11.4 监听系统属性示例
swift
// Swift - 监听 UIScrollView 的 contentOffset
class MyViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
var scrollObservation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
scrollObservation = scrollView.observe(.contentOffset, options: .new) { sv, change in
let offset = change.newValue ?? .zero
print("滚动到: (offset)")
}
}
}
// Swift - 监听 AVPlayer status
var playerObservation: NSKeyValueObservation?
playerObservation = player.observe(.status, options: .new) { player, change in
switch player.status {
case .readyToPlay:
print("播放器就绪")
case .failed:
print("播放失败: (player.error?.localizedDescription ?? "")")
default:
break
}
}
12. 常见崩溃场景
12.1 ⚠️ 忘记移除观察者
less
// ObjC - ❌ 危险:vc 被释放后,person 变更仍会向已释放的 vc 发通知 → BAD ACCESS
@implementation ViewController
- (void)viewDidLoad {
[self.person addObserver:self forKeyPath:@"name" options:0 context:nil];
// 忘记在 dealloc 中移除!
}
// 没有 dealloc 里的 removeObserver
@end
objectivec
// ✅ 正确
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
12.2 ⚠️ 重复添加观察者
objectivec
// ObjC - ❌ 添加两次,通知会触发两次,移除一次后仍有一个残留观察者
[person addObserver:self forKeyPath:@"name" options:0 context:nil];
[person addObserver:self forKeyPath:@"name" options:0 context:nil]; // 重复!
⚠️ KVO 不会自动去重,重复添加会导致通知被触发多次。
12.3 ⚠️ 移除不存在的观察者
csharp
// ObjC - ❌ 移除一个从未添加过的观察者 → NSRangeException 崩溃
[person removeObserver:self forKeyPath:@"name"]; // person 从来没被观察过
12.4 ⚠️ 子类和父类 context 冲突
kotlin
// ❌ 不传 context,父类也观察同 key,子类无法区分通知来源
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
// 如果父类没实现这个方法,会崩溃:NSInternalInconsistencyException
objectivec
// ✅ 正确:用 static context 区分,不是自己的交给 super 处理
static void *MyContext = &MyContext;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == MyContext) {
// 处理自己的
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
12.5 ⚠️ 线程安全问题
scss
// ⚠️ KVO 通知在哪个线程修改属性,就在哪个线程触发回调
// 如果在子线程修改属性,回调也在子线程,直接更新 UI 会崩溃!
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.model.progress = 0.5; // 触发 KVO,回调在子线程
});
// ✅ 回调中切回主线程
- (void)observeValueForKeyPath:... {
dispatch_async(dispatch_get_main_queue(), ^{
// 更新 UI
});
}
13. KVO 的替代方案
13.1 对比表
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| KVO | 系统内置,无依赖;能监听系统类(AVPlayer/UIScrollView 等) | 字符串类型不安全;需手动管理生命周期;调试难 | 监听系统属性;ObjC 项目 |
| Combine | 类型安全;函数式;生命周期自动管理(AnyCancellable) | iOS 13+;学习曲线 | Swift 新项目;响应式架构 |
| RxSwift | 功能强大;iOS 9+ 支持 | 三方依赖;包大;学习曲线高 | 重度响应式项目 |
| NSNotification | 解耦彻底;一对多 | 无类型安全;需手动移除;传值靠 userInfo 字典 | 跨模块广播 |
| Delegate | 简单直接;类型安全 | 一对一;需额外协议 | 明确的回调关系 |
| 闭包/Block回调 | 灵活;局部化 | 注意循环引用 | 简单单次回调 |
13.2 Combine 替代 KVO
scss
// Swift - Combine(iOS 13+)
import Combine
class ViewController: UIViewController {
let counter = Counter()
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 方式1:publisher(for:) 直接订阅 KVO 属性
counter.publisher(for: .count)
.sink { newValue in
print("count: (newValue)")
}
.store(in: &cancellables)
// 方式2:@Published 属性(需要 ObservableObject)
}
// cancellables 释放时自动取消订阅,无需 removeObserver
}
13.3 何时坚持用 KVO
- 监听系统框架的属性 (
AVPlayer.status、UIScrollView.contentOffset、NSOperation.isFinished) - 需要兼容 iOS 12 及以下
- 在 ObjC 代码库中
14. 实际应用场景
14.1 监听 UIScrollView contentOffset
swift
// Swift
var scrollObservation: NSKeyValueObservation?
scrollObservation = scrollView.observe(.contentOffset, options: .new) { [weak self] sv, change in
guard let self = self, let offset = change.newValue else { return }
// 根据滚动位置更新导航栏透明度
let alpha = min(offset.y / 100.0, 1.0)
self.navigationController?.navigationBar.alpha = alpha
}
14.2 监听 AVPlayer 状态
swift
// Swift
var playerStatusObservation: NSKeyValueObservation?
var playerItemObservation: NSKeyValueObservation?
func setupPlayer() {
let player = AVPlayer(url: videoURL)
playerStatusObservation = player.observe(.status, options: [.new]) { player, _ in
switch player.status {
case .readyToPlay: print("就绪,可以播放")
case .failed: print("加载失败: (player.error!)")
case .unknown: print("未知状态")
@unknown default: break
}
}
playerItemObservation = player.currentItem?.observe(.isPlaybackLikelyToKeepUp) { item, _ in
print("缓冲充足: (item.isPlaybackLikelyToKeepUp)")
}
}
14.3 监听 NSOperation 进度
objectivec
// ObjC
[operation addObserver:self
forKeyPath:@"isFinished"
options:NSKeyValueObservingOptionNew
context:MyOperationContext];
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == MyOperationContext) {
if ([change[NSKeyValueChangeNewKey] boolValue]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self operationDidFinish:(NSOperation *)object];
});
}
}
}
14.4 实现进度条绑定
swift
// Swift - 将 model 的 progress 绑定到 UI
class DownloadViewController: UIViewController {
var task: DownloadTask!
var progressObservation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
progressObservation = task.observe(.progress, options: .new) { [weak self] task, change in
DispatchQueue.main.async {
self?.progressView.setProgress(Float(task.progress), animated: true)
}
}
}
}