「iOS」——KVO
源码学习
- iOS底层学习:KVO 底层原理
 
iOS底层学习:KVO 底层原理
KVO
KVO 的全称是 KeyValueObserving,俗称 “键值监听 \",可以用于监听某个对象属性值的改变;KVO 可以通过监听 key,来获得 value 的变化,用来在对象之间监听状态变化。
基本思想:对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的 KVO 接口方法,来自动的通知观察者。
KVO 是苹果提供的一套事件通知机制。KVO 和 NSNotificationCenter 都是 iOS 中观察者模式的一种实现,区别是:NSNotificationCenter 可以是一对多的关系,而 KVO 是一对一的;
注册 KVO 监听
通过[addObserver:forKeyPath:options:context:]方法注册 KVO,这样可以接收到 keyPath 属性的变化事件:
- 
observer:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context:方法。 - 
keyPath:要观察的属性名称,需与属性声明的名称一致。 - 
options:回调方法中收到被观察者的属性的旧值或新值等,对 KVO 机制进行配置,修改 KVO 通知的时机以及通知的内容。 - 
context:上下文,会传递到观察者的函数中,用于区分消息,应当为不同值。 
options所包括的内容:
- 
NSKeyValueObservingOptionNew:change 字典包括改变后的值。 - 
NSKeyValueObservingOptionOld:change 字典包括改变前的值。 - 
NSKeyValueObservingOptionInitial:注册后立刻触发 KVO 通知。 - 
NSKeyValueObservingOptionPrior:值改变前是否也要通知(决定是否在改变前、改变后通知两次)。 
实现 KVO 监听
通过方法[observeValueForKeyPath:ofObject:change:context:]实现 KVO 的监听:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 
- 
keyPath:被观察对象的属性。 - 
object:被观察的对象。 - 
change:字典,存放相关的值,根据options传入的枚举返回新值、旧值。 - 
context:注册观察者时传递的context值。 
移除 KVO 监听
通过方法[removeObserver:forKeyPath:]移除监听;
处理变更通知
每当监听的 keyPath 发生变化时,会在observeValueForKeyPath函数中回调
- (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)object change:(NSDictionary *)change context:(void *)context
change字典保存了变更信息,具体内容取决于注册时的NSKeyValueObservingOptions。
手动KVO(禁用KVO)
KVO 的实现是在注册的 keyPath 的 setter 方法中,自动插入并调用了两个函数:
- (void)willChangeValueForKey:(NSString *)key- (void)didChangeValueForKey:(NSString *)key
手动实现 KVO 需先关闭自动生成 KVO 通知,再手动调用通知方法,可灵活添加判断条件。
关闭自动通知
 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@\"age\"]) { return NO; } else { return [super automaticallyNotifiesObserversForKey:key]; } } 
手动实现 setter 方法
接着手动实现属性的 setter 方法,在setter方法中先调用willChangeValueForKey:接着进行赋值操作,然后调用didChangeValueForKey:
- (void)setAge:(int)theAge { [self willChangeValueForKey:@\"age\"]; age = theAge; [self didChangeValueForKey:@\"age\"]; } 
KVO 和线程
- 
KVO 行为是同步的,在所观察的值发生变化的同一线程上触发,无队列或 Runloop 处理。
 - 
手动或自动调用
didChangeValueForKey:会触发 KVO 通知。 - 
单线程保证(如主队列):
 
- 
确保所有监听某一属性的观察者在 setter 方法返回前被通知到。
 - 
若键观察时附上
NSKeyValueObservingOptionPrior选项,直到observeValueForKeyPath被调用前,监听的属性返回值不变。- 该键对应的值是一个 
NSNumber(BOOL类型),用于判断当前 KVO 通知是在属性值 变更前(前置通知,值为YES)还是 变更后(后置通知,值为NO)发送。 
 - 该键对应的值是一个 
 
上述两个特点可以有效解决复杂场景下的数据一致性和时序问题
我们看以下代码:
// User.h@interface User : NSObject@property (nonatomic, assign) NSInteger age;@end// ViewController.m- (void)viewDidLoad { [super viewDidLoad]; // 注册 KVO 监听 [self.user addObserver:self forKeyPath:@\"age\"  options:NSKeyValueObservingOptionNew  context:nil]; // 主线程修改 age dispatch_async(dispatch_get_main_queue(), ^{ self.user.age = 20; NSLog(@\"主线程修改 age 为 20\"); }); // 子线程同时修改 age dispatch_async(dispatch_get_global_queue(0, 0), ^{ self.user.age = 30; NSLog(@\"子线程修改 age 为 30\"); });}// KVO 回调- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@\"age\"]) { NSNumber *newAge = change[NSKeyValueChangeNewKey]; NSLog(@\"KVO 接收到 age 变化: %@\", newAge); }}
如果 KVO 是多线程的
- 可能出现通知丢失:主线程和子线程同时修改 
age,观察者可能只收到最后一次通知(如只收到 30,丢失 20)。 - 可能出现通知顺序错乱:观察者先收到 30 的通知,再收到 20 的通知,导致逻辑混乱。
 
单线程的保证
- 原子性:KVO 会在 setter 方法返回前同步且顺序地通知所有观察者,确保:
- 所有观察者都能收到每一次变化。
 - 通知顺序与 setter 调用顺序一致(先收到 20,再收到 30)。
 
 
再来学习NSKeyValueObservingOptionPrior。该属性主要应用在复杂数据更新与 UI 动画同步
// 注册 KVO,带上 prior 选项[self.dataSource addObserver:self  forKeyPath:@\"items\"  options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior)  context:nil];// KVO 回调- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@\"items\"]) { // 1. 先收到 prior 通知(change[NSKeyValueChangeNotificationIsPriorKey] = @YES) if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) { // 准备动画(此时 items 还是旧值) [self.tableView beginUpdates]; } // 2. 再收到实际变化通知 else { // 执行动画(此时 items 已是新值) [self.tableView endUpdates]; // 自动触发 insert/delete 动画 } }}
如果没有 prior 选项
- 直接在 
endUpdates时才知道数据变化,无法提前准备动画。 - 可能导致 UI 闪烁或动画不连贯。
 
prior 选项的作用
- 分阶段通知:
- 第一次通知:在属性值实际变更前触发(
NSKeyValueChangeNotificationIsPriorKey = @YES),此时属性值仍为旧值。 - 第二次通知:在属性值变更后触发(默认行为),此时属性值已更新。
 
 - 第一次通知:在属性值实际变更前触发(
 - 实际应用:
- 在第一次通知时,计算新旧数据的差异(如哪些行需要插入 / 删除)。
 - 在第二次通知时,执行 
beginUpdates/endUpdates,让表格视图平滑过渡。 
 
KVO 实现原理
KVO 通过isa-swizzling实现,基本流程如下:
isa-swizzling 的本质:
修改对象的类型:通过修改对象的
isa指针,使其指向另一个类,从而改变对象的行为。
- 
创建派生类:编译器自动为被观察对象创建派生类(如
NSKVONotifying_XXX),将被观察实例的isa指向该派生类,派生类的superclass指向原类。 - 
重写方法:若注册了某属性的观察,派生类会重写该属性的 setter 方法,并添加通知代码。
 - 
消息传递:Objective-C 通过
isa指针找到对象所属类,调用派生类重写后的方法,触发通知。 
派生类重写的方法
- setter 方法:插入
willChangeValueForKey:和didChangeValueForKey:调用,触发通知。 
- (void)willChangeValueForKey:(NSString *)key;- (void)didChangeValueForKey:(NSString *)key;
然后在 didChangeValueForKey: 中,去调用:
- (void)observeValueForKeyPath:(nullable NSString *)keyPathofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- class 方法:返回原类,隐藏子类存在,避免
isKindOfClass判断异常。 
- (Class)class { return class_getSuperclass(object_getClass(self)); } 
- 
dealloc 方法:释放 KVO 相关资源。
 - 
_isKVOA 方法:返回
YES,标识该类为 KVO 生成的子类。 

验证 isa 指向示例
#import <Foundation/Foundation.h> #import <objc/runtime.h> @interface ObjectA: NSObject @property (nonatomic) NSInteger age; @end @implementation ObjectA @end @interface ObjectB: NSObject @end @implementation ObjectB - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@\"%@\", change); } @end int main(int argc, const char * argv[]) { @autoreleasepool { ObjectA *objA = [[ObjectA alloc] init]; ObjectB *objB = [[ObjectB alloc] init]; [objA addObserver:objB forKeyPath:@\"age\" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; NSLog(@\"%@\", [objA class]); // 输出:ObjectA(表面类型)  NSLog(@\"%@\", object_getClass(objA)); // 输出:NSKVONotifying_ObjectA(实际类型)  } return 0; } 
- 
class方法返回对象所属的类(原类)。 - 
object_getClass返回对象的isa指向的实际类(派生类)。 

KVO 注意事项
- 内存管理:
 
- 
addObserver与removeObserver需成对调用,避免观察者释放后仍接收通知导致 Crash。 - 
KVO 不对观察者强引用,需注意观察者生命周期。否则会导致观察者被释放带来的Crash。
 
- 
方法实现:观察者必须实现
observeValueForKeyPath:ofObject:change:context:方法,否则崩溃。 - 
KeyPath 安全:在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash。我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。
 - 
数组监听:默认仅监听数组对象本身变化,需通过
mutableArrayValueForKey操作数组或手动触发通知来监听元素变化。 
问题总结
- 
**直接修改成员变量是否触发 KVO?**不会。KVO 本质是替换 setter 方法,仅通过 setter 或 KVC 修改属性值时触发。
 - 
**KVC 修改属性会触发 KVO 吗?**会。
setValue:forKey:会调用willChangeValueForKey和didChangeValueForKey,触发监听器回调。 - 
如何监听数组元素变化?
 
- 
使用
NSMutableArray并通过mutableArrayValueForKey获取数组,其操作会自动触发通知。 - 
手动调用
willChangeValueForKey和didChangeValueForKey触发通知。 
当数组中的元素发生变化时,手动触发KVO通知即可实现监听。具体实现方式如下:
使用NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld选项来监听可变数组中的元素变化。这两个选项会在KVO通知中包含新旧值的信息,因此可以在观察者中获取到数组中元素的变化。
 代码如下:
[observedObject addObserver:self forKeyPath:@\"myArray\" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
//观察者中实现.- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@\"myArray\"]) { NSArray *oldArray = change[NSKeyValueChangeOldKey]; NSArray *newArray = change[NSKeyValueChangeNewKey]; // 处理数组元素的变化 }}


