这个方法是 KVO (Key-Value Observing) 机制的核心,用于为某个对象注册一个观察者,以便在指定属性的值发生变化时收到通知。

(图片来源网络,侵删)
方法签名
// Objective-C
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
// Swift (通过 KVO 机制,语法更简洁)
// 通常使用 @objcMembers 或 dynamic 修饰,或者继承自 NSObject
// 注册观察
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
// 移除观察
deinit {
// 通常在这里移除观察者
}
参数详解
下面我们逐一分析每个参数的作用和可选值。
observer: NSObject (观察者)
- 类型:
NSObject(或其子类) - 作用: 指定一个对象,这个对象将负责接收属性变化的通知,当被观察对象的
keyPath对应的值发生改变时,observer对象的observeValueForKeyPath:ofObject:change:context:方法会被调用。 - 关键点:
- 观察者必须遵守
NSKeyValueObserving协议,虽然这个协议是隐式的(任何NSObject都默认遵循),但你的类需要实现observeValueForKeyPath:ofObject:change:context:方法来处理通知。 - 观察者和被观察的对象可以是同一个对象,但通常不推荐,这可能会导致逻辑混乱。
- 移除观察者: 当观察者对象(通常是
ViewController)被销毁时,必须调用removeObserver:forKeyPath:来移除观察,否则,当被观察对象发生变化时,会向一个已经不存在的对象发送消息,导致程序崩溃,最佳实践是在观察者的deinit方法中移除所有观察。
- 观察者必须遵守
keyPath: NSString (被观察的属性路径)
- 类型:
NSString - 作用: 指定一个点分隔的路径字符串,用于标识要观察的属性,这个路径可以指向一个对象本身的属性,也可以指向其嵌套对象的属性。
- 关键点:
- 简单属性: 如果要观察对象自身的
name属性,keyPath@"name"。 - KVC 兼容:
keyPath的语法与 Key-Value Coding (KVC) 完全一致。 - 点语法: 对于嵌套属性,使用点语法,要观察一个
Person对象的address属性中的city属性,keyPath@"address.city"。 - 必须是属性:
keyPath必须对应一个真实的、符合 KVO 规范的属性,直接观察实例变量(_ivar)是无效的。
- 简单属性: 如果要观察对象自身的
options: NSKeyValueObservingOptions (通知选项)
-
类型:
NSKeyValueObservingOptions(这是一个NS_OPTIONS枚举,可以组合使用) -
作用: 定义在属性值发生变化时,传递给
observeValueForKeyPath...方法的change字典中包含哪些额外信息,它决定了你何时收到通知以及通知的详细程度。 -
可选值:
(图片来源网络,侵删)NSKeyValueObservingOptions.new(0x01): 在change字典中包含变化后(新)的值,键为NSKeyValueChangeNewKey。NSKeyValueObservingOptions.old(0x02): 在change字典中包含变化前(旧)的值,键为NSKeyValueChangeOldKey。NSKeyValueObservingOptions.initial(0x01): 在观察建立之后,立即发送一次通知,这次通知的change字典中会包含属性的初始值,这对于确保在对象创建后就能立即获取到属性的初始状态非常有用。NSKeyValueObservingOptions.prior(0x02): 在值改变之前和改变之后都发送通知,改变前的通知中,change字典里的NSKeyValueChangeNotificationIsPriorKey键对应的值为@YES。
-
常用组合:
[.new, .initial]: 这是最常用的组合之一,它确保你既能收到属性初始值的通知,也能在每次属性变化后收到新值。.new: 只关心变化后的新值。.old: 只关心变化前的旧值(在移除某个对象前,需要知道它原来的值)。.prior: 用于在值改变前执行一些操作,比如取消动画或更新 UI 状态。
context: void * (上下文)
- 类型:
void *(在 Swift 中是UnsafeMutableRawPointer?) - 作用: 一个可选的、不透明的指针,用作传递给观察者的自定义数据,它主要用于解决一个关键问题:观察者冲突。
- 关键点:
- 解决冲突: 当一个对象(
ViewController)同时观察了多个不同对象的同一个keyPath时,在observeValueForKeyPath...方法中,你很难判断这个变化是来自哪个被观察对象的,通过为每个addObserver调用传入不同的context,你就可以在回调方法中通过context参数来区分不同的通知来源。 - 避免使用
keyPath判断: 强烈建议不要在observeValueForKeyPath...中通过if ([keyPath isEqualToString:@"..."])来分支处理,因为如果keyPath相同但context不同,这种判断就会失效。context是更可靠、更优雅的解决方案。 - 如何使用:
- 定义一个唯一的指针作为上下文,通常使用
static void * const MyContext = &MyContext;这样的方式。 - 在
addObserver时传入这个指针。 - 在
observeValueForKeyPath...方法中,检查传入的context是否与你传入的一致,然后执行相应的逻辑。
- 定义一个唯一的指针作为上下文,通常使用
- 传递
nil: 如果你不需要解决冲突,或者只观察一个对象,可以安全地传递nil。
- 解决冲突: 当一个对象(
代码示例 (Swift)
下面是一个完整的 Swift 示例,展示了所有参数的用法。
import Foundation
// 1. 定义被观察的模型
class Person: NSObject {
@objc dynamic var name: String = "Initial Name"
@objc dynamic var age: Int = 0
// 为了演示嵌套 keyPath
@objc dynamic var address: String = "Unknown"
}
// 2. 定义观察者
class MyViewController: NSObject {
// 定义两个不同的 context,用于区分观察来源
private var nameObservationContext = UnsafeMutableRawPointer.allocate(byteCount: 1, alignment: 1)
private var ageObservationContext = UnsafeMutableRawPointer.allocate(byteCount: 1, alignment: 1)
var person = Person()
override init() {
super.init()
setupKVO()
}
deinit {
print("MyViewController is being deallocated. Removing observers...")
// 3. 在 deinit 中移除观察者,防止内存泄漏和崩溃
person.removeObserver(self, forKeyPath: #keyPath(Person.name), context: nameObservationContext)
person.removeObserver(self, forKeyPath: #keyPath(Person.age), context: ageObservationContext)
// 释放内存
nameObservationContext.deallocate()
ageObservationContext.deallocate()
}
func setupKVO() {
// 观察 name 属性
// 参数解析:
// observer: self (当前 ViewController)
// keyPath: #keyPath(Person.name) (编译器安全的属性名)
// options: [.new, .initial] (关心新值和初始值)
// context: nameObservationContext (自定义上下文,用于区分)
person.addObserver(self, forKeyPath: #keyPath(Person.name), options: [.new, .initial], context: nameObservationContext)
// 观察 age 属性
person.addObserver(self, forKeyPath: #keyPath(Person.age), options: [.new], context: ageObservationContext)
}
// 4. 实现 KVO 回调方法
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
// 5. 使用 context 来判断通知来源
if context == nameObservationContext {
print("--- Name Observation ---")
// 可以安全地断言 change 字典中存在新值
if let newName = change?[.newKey] as? String {
print("Person's name changed to: \(newName)")
}
} else if context == ageObservationContext {
print("--- Age Observation ---")
if let newAge = change?[.newKey] as? Int {
print("Person's age changed to: \(newAge)")
}
} else {
// context 不匹配,调用父类的实现,避免潜在的 bug
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}
// --- 测试 ---
let viewController = MyViewController()
// 模拟属性变化
// 这会触发 KVO 通知
viewController.person.name = "Alice"
viewController.person.age = 30
// 再次变化
viewController.person.name = "Bob"
viewController.person.age = 31
| 参数 | 类型 | 作用 | 最佳实践 |
|---|---|---|---|
observer |
NSObject |
接收通知的对象。 | 必须实现 observeValueForKeyPath...,并在 deinit 中移除观察。 |
keyPath |
NSString |
要观察的属性路径。 | 使用点语法支持嵌套属性,确保是有效的 KVC 属性路径。 |
options |
NSKeyValueObservingOptions |
定义通知内容和时机。 | 根据需要组合使用 .new, .old, .initial, .prior。.new 和 .initial 是最常用的组合。 |
context |
void * |
用于区分不同通知来源的指针。 | 强烈推荐使用,以避免 keyPath 冲突,使代码更健壮。 |
理解这四个参数是掌握 KVO 的关键,现代 Swift 更推荐使用 Combine 框架或基于闭包的第三方库(如 RxSwift, Bond)来进行响应式编程,因为它们更安全、更易于使用,但在处理 Objective-C 代码或特定系统框架时,KVO 仍然是一个不可或缺的工具。

(图片来源网络,侵删)
