一、基础
KVO(Key-Value Observing,键值观察)是 Cocoa 提供的一种机制,它允许我们观察属性的变化并做出响应。这种机制非常强大,广泛应用于各种编程场景,如数据绑定、状态变化监控等。在深入了解 KVO 之前,我们先从 KVO 的基本概念开始,然后逐步探讨其深层次应用和一些使用实践的注意事项。
一、KVO 基本概念
1. 定义和原理
KVO 是一种基于观察者设计模式的实现,它允许一个对象观察另一个对象的属性变化。当被观察的属性发生变化时,观察者会从“被通知”状态转变到“更新”状态。例如,当一个 model 的属性发生变化时,ViewModel 可以观察到变化并相应地更新视图。
- 被观察者:拥有要被观察的属性,一旦属性发生变化,它会通知所有的观察者。被观察者通常是一个
NSObject
的子类。 - 观察者:注册监听被观察者的属性变化,并在变化时获取通知,通过重写
observeValueForKeyPath:ofObject:change:context:
方法做出相应处理。
2. 基本用法
首先,我们定义一个模型对象 User
,并添加对其 name
属性的观察。
Model:User
@interface User : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation User
@end
观察变化
@interface MyObserver : NSObject
@end
@implementation MyObserver
- (void)startObservingUser:(User *)user {
[user addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
}
- (void)stopObservingUser:(User *)user {
[user removeObserver:self forKeyPath:@"name"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"Name changed from %@ to %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
}
}
@end
使用示例
User *user = [[User alloc] init];
MyObserver *observer = [[MyObserver alloc] init];
[observer startObservingUser:user];
user.name = @"NewName"; // This will trigger the KVO notification
二、KVO 的深入特性
1. 自动和手动 KVO
自动 KVO:系统自动支持的大多数属性,遵循简单的属性管理(即使用 @property
定义的属性),不需要手动实现属性的观察。
手动 KVO:需要在属性更新时手动触发 KVO 通知,通常用于更复杂的数据结构或自定义的观察逻辑。
手动实现 KVO
@interface ManualKVOUser : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation ManualKVOUser
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
@end
2. KVO 的依赖键路径
在某些情况下,一个属性的值依赖于其他属性的值。可以重载 keyPathsForValuesAffectingValueForKey:
来声明这种关系。
示例
假设我们有一个类 Rectangle
,它有宽度和高度属性。我们希望当宽度或高度发生变化时,面积属性也会发生变化。
@interface Rectangle : NSObject
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, readonly) CGFloat area;
@end
@implementation Rectangle
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
if ([key isEqualToString:@"area"]) {
return [NSSet setWithObjects:@"width", @"height", nil];
}
return [super keyPathsForValuesAffectingValueForKey:key];
}
- (CGFloat)area {
return self.width * self.height;
}
@end
3. KVO 和集合对象
KVO 也支持集合对象的观察,包括 NSArray
、NSSet
和 NSDictionary
。可以观察集合对象的内容变化,而不仅仅是对集合对象的指针变化。
示例
观察 NSMutableArray
的内容变化:
@interface ListWatcher : NSObject
@end
@implementation ListWatcher
- (void)observeList:(NSMutableArray *)list {
[list addObserver:self
toObjectsAtIndexes:[NSIndexSet indexSetWithIndex:0]
forKeyPath:@"@count"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
}
- (void)dealloc {
[list removeObserver:self forKeyPath:@"@count"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"@count"]) {
NSLog(@"Array count changed: %@", change);
}
}
@end
三、KVO 的注意事项
- 移除观察者:不要忘记在适当的时候移除观察者,否则会导致崩溃。通常在
dealloc
方法中移除。 - ** KVO Context**:在复杂项目中,应为每个 KVO 观察提供唯一的上下文,以避免因不同模块监听同一键路径而产生的冲突。
- 线程安全:确保在合适的线程上下文中添加和移除观察者,以避免数据竞争和崩溃。
- KVO 错误处理:在
observeValueForKeyPath:ofObject:change:context:
方法中处理未知的 keyPath,避免意外的行为:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"expectedKeyPath"]) {
// 处理特殊观察逻辑
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
四、实践中的 KVO 和 RAC 的结合
RAC 可以大大简化和增强 KVO 的使用,使代码更具表达力和可维护性。
结合 RAC 的 KVO
User *user = [[User alloc] init];
RACSignal *nameSignal = [user rac_valuesForKeyPath:@"name" observer:self];
[nameSignal subscribeNext:^(id _Nullable newName) {
NSLog(@"Name changed to %@", newName);
}];
二、底层
KVO(Key-Value Observing,键值观察)的底层实现机制在 Objective-C 中相当复杂,但理解其原理能够帮助我们更好地使用和调试 KVO。KVO 的实现主要依赖于 Objective-C 的运行时特性和动态方法解析机制。
1、KVO 背后的原理
当你对一个对象的某个属性添加观察者后,实际上发生了以下几步:
- 动态创建子类:运行时系统会为被观察的对象动态创建一个新的子类,并将该对象的 isa 指针指向这个子类。
- 重写属性的 setter 方法:在这个新创建的子类中,重写被观察属性的 setter 方法,以便在属性发生变化时,通知所有的观察者。
- 维护观察者列表:这个子类还会维护一个观察者列表,记录所有对该属性添加了观察的观察者。
- 触发通知:当属性的 setter 方法被调用时,会触发 KVO 通知,通知所有的观察者,调用
observeValueForKeyPath:ofObject:change:context:
方法。
二、KVO 的具体实现步骤
1. 动态创建子类
当你调用 addObserver:forKeyPath:options:context:
,运行时系统会动态创建一个子类,该子类的类名并不是静态的,而是通过拼接生成的。例如,如果你对一个 Person
对象添加观察,系统可能会生成一个名为 NSKVONotifying_Person
的子类。
2. 修改 isa 指针
被观察的对象的 isa
指针会被修改为这个新生成的子类。isa
指针用于指向对象所属的类,通过动态改变 isa
指针,可以改变对象的方法实现。
3. 重写属性的 setter 方法
新的子类会重写被观察属性的 setter 方法,以便在属性值被修改后进行通知。该 setter 方法大概如下:
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
willChangeValueForKey:
和 didChangeValueForKey:
方法用于通知系统即将发生一个变化和变化已经发生,系统会在这两个方法之间发送通知给所有的观察者。
三、KVO 的底层实现详解
1. 动态创建子类
在 addObserver:forKeyPath:options:context:
方法被调用时,系统首先检查当前类是否已经有一个对应的 KVO 子类。如果没有,系统会动态创建一个子类并重写相关方法。
// 示例代码,展示动态创建 KVO 子类的步骤
Class originalClass = object_getClass(myObject);
NSString *kvoClassName = [NSString stringWithFormat:@"NSKVONotifying_%@", NSStringFromClass(originalClass)];
// 检查是否已经有这个类
Class kvoClass = NSClassFromString(kvoClassName);
if (!kvoClass) {
// 创建子类
kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
// 重写 setter 方法
SEL setterSelector = @selector(setName:);
Method setterMethod = class_getInstanceMethod(originalClass, setterSelector);
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(kvoClass, setterSelector, (IMP)kvo_setter, types);
// 注册新类
objc_registerClassPair(kvoClass);
}
// 修改 isa 指针
object_setClass(myObject, kvoClass);
2. 重写 setter 方法
重写的 setter 方法不仅会更改属性值,还会确保在属性变化前后发送通知,触发观察者的回调:
void kvo_setter(id self, SEL _cmd, NSString *newName) {
// 获取旧值
NSString *oldName = [self name];
// 通知即将更改
[self willChangeValueForKey:@"name"];
// 执行原始的 setter 方法
void (*originalSetter)(id, SEL, NSString *) = (void (*)(id, SEL, NSString *))class_getMethodImplementation(object_getClass(self), _cmd);
originalSetter(self, _cmd, newName);
// 通知已经更改
[self didChangeValueForKey:@"name"];
}
注意,这里有一个重要的调用 class_getMethodImplementation
,它获取了原始类的 setter 方法实现并调用它以确保属性值的实际改变。
3. 两步通知
willChangeValueForKey:
和 didChangeValueForKey:
方法会触发 KVO 通知。系统会在这两个方法调用之间发送通知给所有注册的观察者。核心实现如下:
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"]; // 通知即将更改值
[super setName:name]; // 调用原始的 setter 方法执行实际更改
[self didChangeValueForKey:@"name"]; // 通知值已经更改
}
四、KVO 的使用细节
1. 自动 KVO 与手动 KVO
如前所述,大多数情况下,KVO 是自动的。但在一些特殊情况或自定义对象中,你可能希望手动管理 KVO 通知。可以通过调用 willChangeValueForKey:
和 didChangeValueForKey:
手动触发 KVO 通知。
- (void)setCustomName:(NSString *)name {
[self willChangeValueForKey:@"customName"];
_customName = name;
[self didChangeValueForKey:@"customName"];
}
2. Dictionary Observing
在 KVO 中,用字典可以包含多个信息。change
字典包含了变化的信息,如新旧值:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"Old value: %@", change[NSKeyValueChangeOldKey]);
NSLog(@"New value: %@", change[NSKeyValueChangeNewKey]);
}
}
五、注意事项
1. 内存管理
释放观察者:在对象销毁前,务必移除所有的观察者,否则可能会引发崩溃。
- (void)dealloc {
[observedObject removeObserver:self forKeyPath:@"name"];
}
可以利用 dealloc
方法中执行清理操作,确保对象在销毁时妥善移除所有观察者。但要特别注意现代 Objective-C 允许在 dealloc
方法中直接调用 release
,因此需要小心处理。
2. 线程安全
在多线程环境中,添加和移除观察者时需要确保线程安全性。可以考虑使用同步机制来保护这些操作。
3. KVO Context
在实践中,通常会为不同的 KVO 监听提供唯一的上下文指针,以避免由于监听同一键路径引起的冲突。上下文指针通常使用静态指针变量。
static void *NameChangeContext = &NameChangeContext;
[observedObject addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NameChangeContext];
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if (context == NameChangeContext) {
// handle change
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
理解 KVO 的底层实现原理,可以帮助我们更有效地利用这一强大的机制。KVO 通过动态创建子类、重写 setter 方法和维护观察者列表,为我们提供了一种简洁而高效的观察属性变化的方式。安全的使用 KVO,包括管理内存、调试和避免线程问题,对于构建稳定和高效的应用至关重要。
标签:name,KVO,void,iOS,context,127,self,change From: https://www.cnblogs.com/chglog/p/18308139