KVO 详解 —— iOS/ObjC 完整学习指南

48 阅读15分钟

1. KVO 是什么

1.1 概念

KVO(Key-Value Observing)是 Cocoa 提供的一种观察者模式实现机制,允许对象监听另一个对象特定属性的变化,当该属性值发生变化时,系统会自动通知所有注册的观察者。

KVO 基于 KVC(Key-Value Coding) 构建,属性必须支持 KVC 才能被 KVO 监听。

1.2 观察者模式回顾

被观察对象 (Subject)
    │
    │ 属性发生变化
    ▼
KVO Runtime 机制
    │
    ├──▶ 观察者 A:observeValueForKeyPath:...
    ├──▶ 观察者 B:observeValueForKeyPath:...
    └──▶ 观察者 C:observeValueForKeyPath:...

1.3 KVO vs NSNotification vs Delegate 对比

特性KVONSNotificationDelegate
监听目标特定对象的特定属性全局通知中心发出的事件特定协议方法
耦合度低(无需修改被观察类)低(需要知道通知名)高(需实现协议)
观察者数量多对一多对多一对一
线程安全⚠️ 通知在属性变更的线程触发⚠️ 通知在 post 的线程触发取决于调用方
类型安全❌ 运行时字符串 keyPath❌ 运行时字符串通知名✅ 编译时检查
适用场景属性值变化监听系统/全局事件回调单一代理
崩溃风险⚠️ 忘记移除会崩溃✅ 相对安全✅ 相对安全
Swift 现代写法✅ 有 observe API✅ NotificationCenter✅ 协议

1.4 KVO 的优势场景

  • 不想(或不能)修改被观察对象的源码
  • 需要同时观察多个属性
  • 需要观察第三方框架的属性(如 AVPlayer.statusUIScrollView.contentOffset

2. 基础用法

2.1 注册观察者

// ObjC
[self.person addObserver:self
             forKeyPath:@"name"
                options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                context:nil];
// Swift(传统方式,兼容 ObjC)
person.addObserver(self,
                   forKeyPath: "name",
                   options: [.new, .old],
                   context: nil)

2.2 参数详解

参数类型说明
observerid观察者对象,必须实现 observeValueForKeyPath:ofObject:change:context:
forKeyPathNSString *要观察的属性路径,支持点语法(如 "address.city"
optionsNSKeyValueObservingOptions控制通知内容(见第 3 节)
contextvoid *任意指针,用于标识和区分观察(见第 6 节)

2.3 实现回调

// 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(传统方式)
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)

// 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 枚举说明
NewNSKeyValueObservingOptionNew.newchange 字典包含变更后的新值(NSKeyValueChangeNewKey
OldNSKeyValueObservingOptionOld.oldchange 字典包含变更前的旧值(NSKeyValueChangeOldKey
InitialNSKeyValueObservingOptionInitial.initial注册时立即触发一次通知(旧值不可用)
PriorNSKeyValueObservingOptionPrior.prior在值变更之前额外触发一次通知

3.2 各选项效果演示

NSKeyValueObservingOptionNew / Old

// ObjC
[obj addObserver:self
     forKeyPath:@"value"
        options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
        context:nil];

// change 字典示例:
// {
//   NSKeyValueChangeKindKey: 1 (NSKeyValueChangeSetting),
//   NSKeyValueChangeNewKey: @"新值",
//   NSKeyValueChangeOldKey: @"旧值"
// }

NSKeyValueObservingOptionInitial

// ObjC —— 注册时会立即触发一次,可用于初始化 UI
[obj addObserver:self
     forKeyPath:@"value"
        options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
        context:nil];
// ✅ 适合用来初始化 UI 状态,不需要手动调一次 update 方法
// Swift
obj.addObserver(self, forKeyPath: "value", options: [.new, .initial], context: nil)

NSKeyValueObservingOptionPrior

// ObjC —— 值变更前后各触发一次
// 变更前的通知:change[NSKeyValueChangeNotificationIsPriorKey] == @YES
[obj addObserver:self
     forKeyPath:@"value"
        options:NSKeyValueObservingOptionPrior | NSKeyValueObservingOptionNew
        context:nil];
- (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 时

// ⚠️ options 为 0 时,change 字典里既没有 new 也没有 old,只有 kind
[obj addObserver:self forKeyPath:@"value" options:0 context:nil];
// 此时 change 只包含 NSKeyValueChangeKindKey

4. observeValueForKeyPath 回调解析

4.1 方法签名

- (void)observeValueForKeyPath:(NSString *)keyPath    // 被观察的属性路径
                      ofObject:(id)object              // 被观察的对象
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change  // 变更信息字典
                       context:(void *)context;        // 注册时传入的 context

4.2 change 字典的所有 Key

Key 常量Swift 枚举类型说明
NSKeyValueChangeKindKey.kindKeyNSNumber变更类型(见下方枚举)
NSKeyValueChangeNewKey.newKeyid新值(需设置 .new option)
NSKeyValueChangeOldKey.oldKeyid旧值(需设置 .old option)
NSKeyValueChangeIndexesKey.indexesKeyNSIndexSet集合变更时的索引(插入/删除/替换)
NSKeyValueChangeNotificationIsPriorKey.notificationIsPriorKeyNSNumber(BOOL)是否为 Prior 通知

4.3 NSKeyValueChangeKindKey 枚举

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting     = 1,  // 普通属性赋值
    NSKeyValueChangeInsertion   = 2,  // 集合元素插入
    NSKeyValueChangeRemoval     = 3,  // 集合元素删除
    NSKeyValueChangeReplacement = 4,  // 集合元素替换
};
// 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
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

// ⚠️ 如果不是自己注册的 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 移除方法

// ObjC —— 指定 keyPath(推荐)
[self.person removeObserver:self forKeyPath:@"name"];

// ObjC —— 指定 keyPath + context(最精确,推荐用于有继承关系的场景)
[self.person removeObserver:self forKeyPath:@"name" context:&MyObservingContext];
// Swift
person.removeObserver(self, forKeyPath: "name")
person.removeObserver(self, forKeyPath: "name", context: &myContext)

5.2 正确的移除时机

// ObjC —— 在 dealloc 中移除
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
    [self.person removeObserver:self forKeyPath:@"age"];
}
// Swift —— 在 deinit 中移除(传统方式)
deinit {
    person.removeObserver(self, forKeyPath: "name")
}

5.3 ⚠️ 崩溃场景一览

// ⚠️ 场景 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 ✅ 安全移除模式

// ✅ 方案一:用 @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];
}
// ✅ 方案三(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 * 作为唯一标识

// ✅ 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
// 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 而不是字符串

// ❌ 不推荐:用字符串作 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

注册 KVO 前:
    person 对象
    ┌──────────────┐
    │ isa → Person │ ──→ Person 类(正常的 setter)
    └──────────────┘

注册 KVO 后:
    person 对象
    ┌────────────────────────────────┐
    │ isa → NSKVONotifying_Person    │ ──→ 动态子类(重写了 setter)
    └────────────────────────────────┘

7.2 动态子类 NSKVONotifying_XXX

当你对 Person 类的对象注册第一个 KVO 时,Runtime 会:

  1. 动态创建 NSKVONotifying_Person 类(Person 的子类)
  2. 重写被观察属性的 setter 方法
  3. 重写 class 方法(返回原始的 Person,隐藏实现细节)
  4. 重写 dealloc 方法(清理 KVO 状态)
  5. 重写 _isKVOA 方法(标记为 KVO 子类)
  6. 将对象的 isa 指针指向新子类
// 验证代码:注册 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 大致等价于:

// 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 完整流程图

p.name = @"李四"
    │
    ▼
NSKVONotifying_Person.setName:
    │
    ├─ willChangeValueForKey:@"name"
    │       │
    │       └─ 遍历观察者列表,发送 Prior 通知(如果设置了 .prior option)
    │
    ├─ [super setName:] ──→ Person.setName: ──→ _name = @"李四"
    │
    └─ didChangeValueForKey:@"name"
            │
            └─ 遍历观察者列表,调用每个观察者的 observeValueForKeyPath:...

7.5 ⚠️ 注意事项

// ⚠️ 直接修改 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

// ObjC —— 在被观察类中重写这个方法
@implementation Person

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO; // ✅ 关闭 name 属性的自动 KVO
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

@end
// 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 手动触发通知

// ObjC
- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}
// Swift
@objc var name: String = "" {
    willSet { willChangeValue(forKey: "name") }
    didSet  { didChangeValue(forKey: "name") }
}

8.4 批量变更(减少通知次数)

// 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?

// ObjC - ❌ 不会触发 KVO
[self.items addObject:newItem]; // 数组本身没变,指针没变

// ✅ 触发 KVO 的正确方式
[[self mutableArrayValueForKey:@"items"] addObject:newItem];

mutableArrayValueForKey: 返回一个代理数组,对它的增删改操作会自动触发 KVO 通知。

9.2 集合变更类型(NSKeyValueChangeKindKey)

说明
NSKeyValueChangeSetting整体替换(普通属性变更)
NSKeyValueChangeInsertion插入元素
NSKeyValueChangeRemoval删除元素
NSKeyValueChangeReplacement替换元素

9.3 监听集合变更

// 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;
    }
}
// ObjC - 触发集合 KVO
DataSource *ds = [[DataSource alloc] init];
[[ds mutableArrayValueForKey:@"items"] addObject:@"item1"];     // 触发 Insertion
[[ds mutableArrayValueForKey:@"items"] removeObjectAtIndex:0];  // 触发 Removal

9.4 自定义集合访问器(更规范的做法)

// 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 基本用法

// 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
// 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 触发效果

// 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 有两个硬性要求:

  1. 类必须继承 NSObject
  2. 被观察属性必须标记为 @objc dynamic
// ✅ 正确:继承 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(推荐)

// 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 - 旧式 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 - 监听 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 ⚠️ 忘记移除观察者

// ObjC - ❌ 危险:vc 被释放后,person 变更仍会向已释放的 vc 发通知 → BAD ACCESS
@implementation ViewController
- (void)viewDidLoad {
    [self.person addObserver:self forKeyPath:@"name" options:0 context:nil];
    // 忘记在 dealloc 中移除!
}
// 没有 dealloc 里的 removeObserver
@end
// ✅ 正确
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
}

12.2 ⚠️ 重复添加观察者

// ObjC - ❌ 添加两次,通知会触发两次,移除一次后仍有一个残留观察者
[person addObserver:self forKeyPath:@"name" options:0 context:nil];
[person addObserver:self forKeyPath:@"name" options:0 context:nil]; // 重复!

⚠️ KVO 不会自动去重,重复添加会导致通知被触发多次。

12.3 ⚠️ 移除不存在的观察者

// ObjC - ❌ 移除一个从未添加过的观察者 → NSRangeException 崩溃
[person removeObserver:self forKeyPath:@"name"]; // person 从来没被观察过

12.4 ⚠️ 子类和父类 context 冲突

// ❌ 不传 context,父类也观察同 key,子类无法区分通知来源
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
// 如果父类没实现这个方法,会崩溃:NSInternalInconsistencyException
// ✅ 正确:用 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 ⚠️ 线程安全问题

// ⚠️ 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

// 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.statusUIScrollView.contentOffsetNSOperation.isFinished
  • 需要兼容 iOS 12 及以下
  • 在 ObjC 代码库中

14. 实际应用场景

14.1 监听 UIScrollView contentOffset

// 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
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 进度

// 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 - 将 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)
            }
        }
    }
}