目录
3. 在 observeValueForKeyPath方法中处理变化
8. 使用 NSKeyValueObservingOptions 获取更详细的信息
前言
这篇博客主要介绍KVO的用法。
1.KVO的概念和基础用法
1.概念
在 iOS 开发中,Key-Value Observing(KVO)是一种强大的机制,允许对象观察其他对象属性的变化。
2.基础用法
KVO的基础用法如下:
- 添加观察者
- 实现观察者方法
- 移除观察者
举一个我们经常使用的一个例子,比如说当前的控制器中有一个UITableView,我们需要监听UITableView滑动时候的偏移量(多一些动画处理,业务逻辑处理等等),这个时候我们可以按照如下的步骤设置KVO.
我们调用下面的addObserver方法,这个方法传递三个参数,第一个参数是要观察的对象,如果是在当前类,我们就传递self,第二个参数是我们要监听的对象的值类型,一般我们监听NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld两个值,第三个参数为context为上下文,我们用来区别当前类中不同的监听对象。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
我们在合适的地方调用addObserver方法,代码如下:
#pragma mark -- KVO
- (void)kvoMethod{
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
然后我们在observerValueForKeyPath中处理自己的业务逻辑,代码如下,我们打印了一下UITableView的偏移量。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"]) {
CGPoint oldOffset = [change[NSKeyValueChangeOldKey] CGPointValue];
CGPoint newOffset = [change[NSKeyValueChangeNewKey] CGPointValue];
NSLog(@"contentOffset changed from %@ to %@", NSStringFromCGPoint(oldOffset), NSStringFromCGPoint(newOffset));
// 在这里处理偏移量的变化
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
在控制器销毁的时候,记得释放监听者。
- (void)dealloc{
[self.tableView removeObserver:self forKeyPath:@"contentOffset"];
}
完整的代码如下(这里只粘贴了KVO部分的代码):
#pragma mark -- KVO
- (void)kvoMethod{
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)dealloc{
[self.tableView removeObserver:self forKeyPath:@"contentOffset"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"]) {
CGPoint oldOffset = [change[NSKeyValueChangeOldKey] CGPointValue];
CGPoint newOffset = [change[NSKeyValueChangeNewKey] CGPointValue];
NSLog(@"contentOffset changed from %@ to %@", NSStringFromCGPoint(oldOffset), NSStringFromCGPoint(newOffset));
// 在这里处理偏移量的变化
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
2.KVO的本质
在 iOS 中KVO 是通过动态子类化和消息转发来实现的。
下面是对 KVO 的本质的详细解释:
1.KVO 的工作原理
1. 动态子类化
当你对某个对象的属性添加观察者时,KVO 会在运行时动态地创建该对象的一个子类,并将这个对象的 isa指针指向这个新的子类。
这个子类重写了被观察属性的 setter 方法。重写后的 setter 方法在调用父类原始的 setter 方法之前和之后,分别调用 willChangeValueForKey: 和 didChangeValueForKey:方法。
2. willChangeValueForKey: 和 didChangeValueForKey
willChangeValueForKey:在属性值改变之前调用,通知系统即将发生变化。
didChangeValueForKey:在属性值改变之后调用,通知系统已经发生变化。
- 这两个方法会触发 KVO 通知,观察者在 observeValueForKeyPath:ofObject:change:context: 方法中接收到这些通知,并进行相应的处理。
3. 消息转发
- 新的子类会重写 class`方法,返回原始类的类型,使得对象看起来还是原来的类。
- 当对该对象发送消息时,如果该消息不是由重写的方法处理的,消息会被转发到原始类的实现。
2.示例代码解释 KVO 的本质
我们可以通过查看对象的 `isa` 指针和类类型来观察 KVO 的工作原理。
#import "KVODemosVC.h"
#import <objc/runtime.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation Person
@end
@interface KVODemosVC ()
@property (nonatomic, strong) Person *person;
@end
@implementation KVODemosVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.person = [[Person alloc] init];
// 创建 UIButton
UIButton *centerButton = [UIButton buttonWithType:UIButtonTypeSystem];
centerButton.backgroundColor = [UIColor darkGrayColor];
centerButton.layer.masksToBounds = YES;
centerButton.layer.borderWidth = 1;
centerButton.layer.cornerRadius = 10;
[centerButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[centerButton setTitle:@"更新对象名称" forState:UIControlStateNormal];
[centerButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
// 禁用自动调整掩码约束转换为自动布局约束
centerButton.translatesAutoresizingMaskIntoConstraints = NO;
// 将按钮添加到视图
[self.view addSubview:centerButton];
// 设置按钮的自动布局约束
[NSLayoutConstraint activateConstraints:@[
[centerButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[centerButton.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
[centerButton.widthAnchor constraintEqualToConstant:150],
[centerButton.heightAnchor constraintEqualToConstant:50]
]];
NSLog(@"触发KVO之前类: %@", object_getClass(self.person));
[self addObserverForPerson];
}
- (void)buttonTapped:(UIButton *)sender {
NSLog(@"Button was tapped!");
self.person.name = [self generateRandomStringWithLength:10];
NSLog(@"触发KVO之后类: %@", object_getClass(self.person));
}
- (void)addObserverForPerson {
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
id oldValue = [change objectForKey:NSKeyValueChangeOldKey];
id newValue = [change objectForKey:NSKeyValueChangeNewKey];
NSLog(@"监听的属性:%@\t监听的对象:%@\t 修改之前:%@\t 修改之后 %@\t", keyPath, object, oldValue, newValue);
NSLog(@"person1类父类:%@",NSStringFromClass([[self.person class] superclass]));
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
NSLog(@"移除KVO之后: %@", object_getClass(self.person));
}
- (NSString *)generateRandomStringWithLength:(NSInteger)length {
NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
NSMutableString *randomString = [NSMutableString stringWithCapacity:length];
for (NSInteger i = 0; i < length; i++) {
u_int32_t rand = arc4random_uniform((u_int32_t)[letters length]);
unichar c = [letters characterAtIndex:rand];
[randomString appendFormat:@"%C", c];
}
return randomString;
}
@end
在上面的实例代码中,我们在页面中监听person类的name属性变化,当我们点击按钮的时候,会随机生成一个字符串,然后把这个字符串赋值给我们要监听的person类的name属性。我们分别再出发KVO之前和之后打印person的类型。
运行代码之后,控制台打印信息如下:
图1.KVO控制台打印信息
1.添加观察者前后的类类型
为了验证这个问题,我们分别在添加KVO前后分别打印出person类型。
从上述控制台的打印信息我们可以看到:
在添加观察者之前,person 的类是 Person。
在添加观察者之后,person 的类变成了一个新的子类,这个子类的名字通常是 _NSKVONotifying_Person。
移除观察者后,person`的类又变回Person。
2.动态子类验证
当 addObserver:forKeyPath:options:context: 被调用时,系统动态创建了一个新的子类 _NSKVONotifying_Person,并将 person 的 isa指针指向这个子类。
我们KVO打印前后分别打印下person类的元类对象,会发现动态生成的_NSKVONotifying_Person的isa指针指向自己的元类对象。
3.setter方法验证
在使用KVO的过程中,被观察的对象会调用自己的方法。
然后我们打一个断点,使用lldb命令看一下添加KVO之前被观察对象的实现。
NSLog(@"触发KVO之前set方法地址: %p", [self.person methodForSelector:@selector(setName:)]);
控制台打印如下:
图2.添加KVO之前修改name属性的控制台打印信息
从控制台打印信息来看,当我们修改person的name属性的时候调用的是person的setName方法。
我们在触发KVO之后,也在控制台打印下person的实现过程:
图3.添加KVO之后修改name属性的时候控制台打印信息
通过对比我们可以看到添加KVO之后,当我们修改name属性的时候,会调用Foundation框架的_NSSetObjectValueAndNotify方法。
在动态子类中,name属性的 setter 方法会在设置新值前后,分别调willChangeValueForKey:和 didChangeValueForKey:。
为了验证这个结论,我们在Person的类中添加打印函数,看一下调用顺序:
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"==========>>>>>willChangeValueForKey被调用<<<<<==========");
}
- (void)setName:(NSString *)name{
_name = name;
NSLog(@"setName方法被调用");
}
- (void)didChangeValueForKey:(NSString *)key{
[super didChangeValueForKey:key];
NSLog(@"==========>>>>>didChangeValueForKey被调用<<<<<==========");
}
运行代码之后,控制台打印信息如下:
图4.控制台打印信息
我们可以得出结论:当我们使用KVO的时候,首先会调用Foundation中的_NSSetObjectValueAndNotify方法,这个方法中首先会调用willChangeValueForKey方法,然后调用被观察的类的set方法修改属性,修改完成之后,调用didChangeValueForKey方法。
4.消息转发
新的子类会拦截对被观察属性的setter调用,并进行处理。
其他消息会被转发到原始类的实现。
通过这些机制,KVO 能够在属性发生变化时通知观察者,实现属性观察的功能。
3.手动触发KVO
了解KVO触发的机制之后,我们可以手动调用willChangeForKey和didChangeForKey即可:
- (void)buttonTapped:(UIButton *)sender {
NSLog(@"Button was tapped!");
// 手动触发 KVO
[self.person willChangeValueForKey:@"name"];
self.person.name = [self generateRandomStringWithLength:10];
[self.person didChangeValueForKey:@"name"];
}
4.KVO常遇到的问题
在使用KVO时,需要注意以下几个方面,以确保代码的安全性和正确性。
1. 添加和移除观察者要成对出现
确保移除观察者:在对象的生命周期结束之前,一定要移除所有观察者。否则会导致应用崩溃。
[self.person removeObserver:self forKeyPath:@"name"];
常见位置:在 dealloc方法中移除观察者,确保对象销毁前已移除观察者。
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
NSLog(@"移除KVO之后: %@", object_getClass(self.person));
}
2. 使用正确的 KVO 选项
常用选项:
NSKeyValueObservingOptionNew:获取新值。
NSKeyValueObservingOptionOld:获取旧值。
示例:
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
3. 在 observeValueForKeyPath方法中处理变化
检查 keyPath:确保正确处理感兴趣的 keyPath。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
// 处理 name 属性的变化
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
4. KVO 与多线程
线程安全:KVO 不是线程安全的。如果观察的属性可能在多个线程中修改,确保在访问和修改属性时使用合适的同步机制。
@synchronized(self) {
self.person.name = @"NewName";
}
5. 观察集合属性
特殊方法:如果观察的是集合(如数组、集合等),使用 KVO的mutableArrayValueForKey: 方法来修改集合,以确保正确发送 KVO 通知。
NSMutableArray *names = [self mutableArrayValueForKey:@"names"];
[names addObject:@"NewName"];
6. 避免嵌套的 KVO 通知
避免循环**:在 KVO 通知中修改同一个属性时,要特别小心,避免触发嵌套的 KVO 通知,从而导致死循环。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
if (!self.isUpdating) {
self.isUpdating = YES;
// 修改属性的代码
self.isUpdating = NO;
}
}
}
7. 使用上下文指针区分不同的 KVO 观察
使用上下文指针:当同一个对象观察多个属性时,可以使用上下文指针来区分不同的观察者。
static void *PersonNameContext = &PersonNameContext;
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
if (context == PersonNameContext) {
// 处理 name 属性的变化
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
8. 使用 NSKeyValueObservingOptions 获取更详细的信息
获取更多信息:通过 `NSKeyValueObservingOptions` 可以获取属性变化的详细信息,如新值、旧值等。
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
9. 调试 KVO
调试工具:使用 `NSLog` 打印属性变化的详细信息,有助于调试 KVO 相关的问题。
NSLog(@"Old value: %@", [change objectForKey:NSKeyValueChangeOldKey]);
NSLog(@"New value: %@", [change objectForKey:NSKeyValueChangeNewKey]);