「iOS」——KVO
源码学习
- iOS底层学习:KVO 底层原理
- KVO
- 注册 KVO 监听
- 实现 KVO 监听
- 移除 KVO 监听
- 处理变更通知
- 手动KVO(禁用KVO)
- 关闭自动通知
- 手动实现 setter 方法
- KVO 和线程
- 如果 KVO 是多线程的
- **单线程的保证**
- 如果没有 prior 选项
- **prior 选项的作用**
- KVO 实现原理
- 派生类重写的方法
- 验证 isa 指向示例
- 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)objectchange:(NSDictionary *)changecontext:(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];// 主线程修改 agedispatch_async(dispatch_get_main_queue(), ^{self.user.age = 20;NSLog(@"主线程修改 age 为 20");});// 子线程同时修改 agedispatch_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)objectchange:(nullable NSDictionary<NSKeyValueChangeKey, id> *)changecontext:(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];// 处理数组元素的变化}
}