源码学习
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 *)keyPath
ofObject:(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 *)keyPath
ofObject:(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];
// 处理数组元素的变化
}
}