iOS开发基础15-KVO属性监听与底层原理
KVO(Key-Value Observing)深度解析:属性监听与底层原理
KVO 是 Objective-C 运行时提供的属性监听机制,允许对象在其他对象的属性发生变化时接收通知。它是响应式编程、数据绑定和许多 UIKit 组件(如 UISlider、UISwitch)的底层基础。
一、KVO 的使用
1. 基本用法
定义被观察对象
// Person.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
// Person.m
@implementation Person
@end
注册监听
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.person.name = @"chg";
self.person.age = 30;
// 注册 KVO 监听
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:nil];
}
#pragma mark - KVO 回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSLog(@"Age changed: %@ → %@",
change[NSKeyValueChangeOldKey],
change[NSKeyValueChangeNewKey]);
}
}
- (void)dealloc {
// 必须移除监听,否则被观察对象释放时可能崩溃
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
2. options 参数详解
| 选项 | 说明 |
|---|---|
NSKeyValueObservingOptionNew |
change 字典包含新值(NSKeyValueChangeNewKey) |
NSKeyValueObservingOptionOld |
change 字典包含旧值(NSKeyValueChangeOldKey) |
NSKeyValueObservingOptionInitial |
注册时立即回调一次(传递初始值) |
NSKeyValueObservingOptionPrior |
值改变前回调一次(change[NSKeyValueChangeNotificationIsPriorKey] = YES),改变后再回调一次 |
3. context 参数的最佳实践
context 是一个 void * 指针,用于区分不同的观察关系。当父类也注册了 KVO 时,使用唯一的 context 可以避免回调冲突:
// 定义唯一的 context 指针(取静态变量的地址)
static void *PersonAgeContext = &PersonAgeContext;
static void *PersonNameContext = &PersonNameContext;
- (void)viewDidLoad {
[super viewDidLoad];
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:PersonAgeContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:PersonNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == PersonAgeContext) {
NSLog(@"age 变化: %@ → %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
} else if (context == PersonNameContext) {
NSLog(@"name 变化: %@ → %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
} else {
// 不属于自己的 context,交给父类处理
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
4. 手动触发 KVO
默认情况下,KVO 自动在 setter 中插入通知。如果需要手动控制通知时机(如批量修改后只通知一次),可以关闭自动通知并手动触发:
// 关闭 age 属性的自动通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (void)setAge:(int)age {
if (_age != age) {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
}
willChangeValueForKey:必须在值改变前调用,didChangeValueForKey:在值改变后调用,两者必须成对出现。
5. 键路径依赖
当一个属性的值依赖于其他属性时,可以通过 keyPathsForValuesAffectingValueForKey: 声明依赖关系,使被依赖属性变化时也触发当前属性的 KVO:
@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy, readonly) NSString *fullName; // 只读,依赖 firstName 和 lastName
@end
@implementation Person
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
// fullName 的值依赖于 firstName 和 lastName
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}
@end
此时监听 fullName,当 firstName 或 lastName 变化时也会收到通知。
6. 注意事项
- 必须移除监听:观察者释放前或被观察对象释放前,必须调用
removeObserver:forKeyPath:。iOS 11+ 对部分 KVO 崩溃做了保护,但最佳实践仍是手动移除。 - 只能监听 setter 修改:直接访问实例变量(如
_age = 30)不会触发 KVO,必须通过 setter 或 KVC(setValue:forKey:)修改。 - 线程安全:KVO 回调在哪个线程修改属性就在哪个线程触发,不保证在主线程。UI 操作需手动切回主线程。
二、Swift 中的 KVO
1. 传统方式(基于 OC 运行时)
Swift 类使用 KVO 必须满足:
- 类继承自
NSObject - 属性标记
@objc dynamic
class Person: NSObject {
@objc dynamic var name: String = ""
@objc dynamic var age: Int = 0
}
class ViewController: UIViewController {
var person: Person!
override func viewDidLoad() {
super.viewDidLoad()
person = Person()
person.addObserver(self, forKeyPath: "age", options: [.old, .new], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "age" {
print("Age changed: \(change?[.oldKey] ?? "") → \(change?[.newKey] ?? "")")
}
}
deinit {
person.removeObserver(self, forKeyPath: "age")
}
}
2. Swift 4+ Block 方式(推荐)
Swift 4 引入了类型安全的 block-based KVO API,返回 NSKeyValueObservation 对象,自动管理生命周期(对象释放时自动移除监听):
class ViewController: UIViewController {
var person: Person!
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
person = Person()
observation = person.observe(\.age, options: [.old, .new]) { person, change in
print("Age changed: \(change.oldValue ?? 0) → \(change.newValue ?? 0)")
}
}
// observation 释放时自动移除监听,无需手动 removeObserver
}
3. Combine 框架(iOS 13+,现代替代方案)
Combine 是 Apple 官方的响应式编程框架,@Published 提供了比 KVO 更优雅的属性观察:
import Combine
class Person: ObservableObject {
@Published var age: Int = 0
}
class ViewController: UIViewController {
let person = Person()
var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
cancellable = person.$age.sink { newValue in
print("Age changed to \(newValue)")
}
}
}
三、KVO 底层原理
1. ISA-Swizzling 与动态子类
KVO 的核心是 ISA-Swizzling(而非 Method Swizzling):
- 当第一次为对象的某个属性注册 KVO 时,系统在运行时动态创建一个名为
NSKVONotifying_原类名的子类(如NSKVONotifying_Person)。 - 将被观察对象的
isa指针从原类修改为这个动态子类(ISA-Swizzling)。 - 动态子类重写被监听属性的 setter 方法,在其中插入
willChangeValueForKey:和didChangeValueForKey:调用。 - 动态子类还重写了以下方法来隐藏自身存在:
class:返回原类(而非动态子类),欺骗外部调用者。dealloc:释放 KVO 相关资源。_isKVOA:内部标记方法,返回YES。
ISA-Swizzling vs Method Swizzling:Method Swizzling 是交换两个已有方法的实现;ISA-Swizzling 是修改对象的
isa指针指向一个新创建的动态子类。两者是不同的技术。
2. 被重写的 setter
动态子类中 setAge: 的实现大致如下:
- (void)setAge:(int)age {
[self willChangeValueForKey:@"age"];
[super setAge:age]; // 调用原类的 setter
[self didChangeValueForKey:@"age"];
}
didChangeValueForKey: 内部会遍历所有注册的观察者,调用其 observeValueForKeyPath:ofObject:change:context: 方法。
3. 验证动态子类存在
可以通过以下代码验证 KVO 的动态子类机制:
NSLog(@"注册前: %s", object_getClassName(self.person));
// 输出: Person
[self.person addObserver:self forKeyPath:@"age" options:0 context:nil];
NSLog(@"注册后: %s", object_getClassName(self.person));
// 输出: NSKVONotifying_Person
NSLog(@"class 方法返回: %@", [self.person class]);
// 输出: Person(class 方法被重写,隐藏了动态子类)
4. 完整通知流程
调用 setter → willChangeValueForKey: → 记录旧值 → 调用原 setter 赋值
→ didChangeValueForKey: → 遍历观察者 → 调用 observeValueForKeyPath:...
四、总结
| 维度 | 说明 |
|---|---|
| 用途 | 监听对象属性变化,实现数据绑定和响应式更新 |
| 触发条件 | 通过 setter 或 KVC 修改属性(直接访问实例变量不触发) |
| 底层原理 | ISA-Swizzling + 动态子类 NSKVONotifying_XXX 重写 setter |
| 必须移除 | 观察者/被观察者释放前移除监听,避免崩溃 |
| Swift 要求 | 继承 NSObject + @objc dynamic 属性 |
| Swift 推荐 | observe(_:options:changeHandler:) block API 或 Combine |
- KVO 是 Objective-C 运行时的特性,基于 ISA-Swizzling 实现,并非 Method Swizzling。
- 使用
context指针区分不同观察关系,避免父类子类回调冲突。 - 手动控制通知时需成对调用
willChangeValueForKey:/didChangeValueForKey:。 - Swift 新项目优先使用 Combine 的
@Published,兼容性要求高时使用 block-based KVO。

浙公网安备 33010602011771号