概述
在 iOS 开发中,KVO
(Key-Value Observing)是 iOS 中一种强大的观察者模式实现,它允许对象监听其他对象特定属性的改变。当被观察的属性发生变化时,观察者会收到相应的通知。本文将对KVO
的原理、使用方式、注意事项以及优缺点深入探讨。
KVO核心机制
KVO
本质是通过 isa-swizzling
技术实现的,具体步骤如下
1.动态子类创建
- 当某个对象第一次被观察时,运行时系统会动态创建该对象类的子类,子类命名通常为
NSKVONotifying_原类名
- 为
NSKVONotifying_原类名
重写观察属性的setter
方法以及delloc
、class
、_isKVOA
方法等
2.settter方法重写
- 在赋值前调用
willChangeValueForKey
- 在赋值后调用
didChangeValueForKey
// KVO 的伪代码实现
- (void)setName:(NSString *)name {
// 1.通知属性即将变化
[self willChangeValueForKey:@"name"];
// 2.调用原始的 setter 方法
_name = name;
// 3.通知属性已经变化
[self didChangeValueForKey:@"name"];
}
- 子类重写
class
方法是为了屏蔽本身,隐藏其存在,不让开发者知道- 当修改实例对象的属性时,会调用
Foundation
的_NSSetXXXValueAndNotify
函数
3.修改isa指针
- 运行时系统会将原对象的
isa
指针指向新创建的子类(NSKVONotifying_MyClass
) - 通过修改
isa
指针,对象的实际类型变为子类,从而使得调用setter
方法时,会优先调用子类中重写的setter
方法
使用方式
Objective-C
@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@end
@implementation Person
@end
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建 Person 对象
self.person = [[Person alloc] init];
self.person.age = 20;
// 注册 KVO
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
// 修改 age 属性的值
self.person.age = 30;
}
// 实现观察者的回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSInteger oldValue = [[change objectForKey:NSKeyValueChangeOldKey] integerValue];
NSInteger newValue = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
NSLog(@"Person's age changed from %ld to %ld", oldValue, newValue);
}
}
// 在不需要监听属性变化时,我们应该及时移除 KVO
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 移除 KVO
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
Swift
class Person: NSObject {
@objc dynamic var age: Int
init(age: Int) {
self.age = age
}
}
class ViewController: UIViewController {
private var person = Person(age: 20)
private var ageObserver: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
// 注册 KVO
ageObserver = person.observe(\.age, options: [.old, .new], changeHandler: { [weak self] (person, change) in
guard let self = self else { return }
if let oldValue = change.oldValue, let newValue = change.newValue {
print("Person's age changed from \(oldValue) to \(newValue)")
}
})
// 修改 age 属性的值
person.age = 30
}
}
func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
ageObserver?.invalidate()
ageObserver = nil
}
注意,为了在
Swift
中使用KVO
,我们需要在age属性前加上@objc dynamic
修饰符。这样才能使age
属性具有动态派发的特性,从而支持KVO
KVO “搞笑”时刻
场景一: 崩溃之王
KVO的经典翻车操作:忘记移除观察者,导致观察者释放后收到通知,直接野指针崩溃
// 手滑少写一句 removeObserver,直接喜提崩溃
[object addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:nil];
// 忘记解除,崩溃警告!
// [object removeObserver:self forKeyPath:@"value"];
场景二:神秘失踪的通知
直接修改实例变量(不用setter)时,KVO完全无感,仿佛在演哑剧
// KVO:我什么都没看到,别问我
_value = 100;
场景三:多线程迷惑行为
KVO
通知可能在任意线程出发,开发者如果忘记切回主线程更新UI,界面直接卡成PPT
优缺点
优点
可以实现对象之间的解耦,一个对象可以监听另一个对象的属性改变,从而及时更新自己的状态。此外,KVO
还可以实现对象的属性验证和数据过滤等功能。
缺点
- 需要手动移除观察者,容易导致内存泄漏
- 回调方法比较混乱,需要手动判断该
KeyPath
- 不方便调试