KVO的基本使用

基本使用

KVO:Key Value Observing(键值监听),用来监听某个对象属性值的改变。

// Person类
@interface Person : NSObject

@property (nonatomic, assign) int age;

@end

@implementation Person

@end

// KVOViewController
@interface KVOViewController ()

@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;

@end

@implementation KVOViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.person1 = [[Person alloc] init];
self.person1.age = 10;

self.person2 = [[Person alloc] init];
self.person2.age = 20;

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 11;
self.person2.age = 21;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@对象的%@属性发生了改变:\n%@",object, keyPath,change);
}

// 打印结果:
2022-11-15 20:08:33.563589+0800 OC对象的本质[81675:15955895] <Person: 0x6000007519c0>对象的age属性发生了改变:
{
kind = 1;
new = 11;
old = 10;
}

注意:在不需要监听的时候需要移除。

- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}

3

4

解释

1、KVO是建立在KVC的基础之上的,即是说给成员变量赋值KVO是无法监听其变化的。

2、context意为上下文信息,我们平时用的时候一般传的NULL,但是苹果官方建议的是把这个参数用起来会更安全、扩展性更强。

5

KVO其他细节

1、是否打开自动观察的开关

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
return YES; // 默认是YES
}

2、返回可能影响监听值的NSSet

// 当writtenData发生改变的时候,downloadProgress就会发生改变
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"writtenData"]; // 只要affectingKeys数组里面的属性发生变化 都会触发downloadProgress的KVO
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}

3、对可变数组的监听

self.person1.booksArr = [NSMutableArray array];
[self.person1 addObserver:self forKeyPath:@"booksArr" options:NSKeyValueObservingOptionNew context:NULL];
[[self.person1 mutableArrayValueForKey:@"booksArr"] addObjectsFromArray:@[@"Hello", @"World"]];

// 打印结果:

6

KVO的本质分析

从上面的例子咱们发现:两个不同的对象person1person2,为什么person1添加了KVO可以监听到属性值的改变?

看起来self.person1.age = 11self.person2.age = 21都是调用的setAge:方法,为什么person1就可以监听到属性值的改变了呢?我们可以大胆猜测一下person1person2setAge:的具体实现肯定不一样了,也就是说person1isaperson2isa指向发生了变化,下面我们来验证一下我们的猜想。

验证一

person1添加KVO前后分别打印person1person2class对象

NSLog(@"person1添加监听之前:person1:%@ person2:%@",object_getClass(self.person1),object_getClass(self.person2));

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"person1添加监听之后:person1:%@ person2:%@",object_getClass(self.person1),object_getClass(self.person2));

打印结果:

2022-11-15 20:21:21.161924+0800 OC对象的本质[81836:15966659] person1添加监听之前:person1:Person person2:Person
2022-11-15 20:21:21.162217+0800 OC对象的本质[81836:15966659] person1添加监听之后:person1:NSKVONotifying_Person person2:Person

如上:我们发现在person1添加了KVO之后,person1isa指向的是NSKVONotifying_Person类,person2isa指向的还是Person类。

验证二

person1添加KVO前后分别打印person1person2setAge:方法的函数地址(IMP):

NSLog(@"person1添加监听之前:person1:%p person1:%p",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"person1添加监听之后:person1:%p person1:%p",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);

打印结果:

2022-11-15 20:32:28.980113+0800 OC对象的本质[81977:15975768] person1添加监听之前:person1:0x102539e70 person1:0x102539e70
2022-11-15 20:32:28.980454+0800 OC对象的本质[81977:15975768] person1添加监听之后:person1:0x7fff207b1cfb person1:0x102539e70

如上:我们发现在person1添加了KVO之后,person1IMPperson2IMP不一样。

通过LLDB指令:

// person1
p (IMP) 0x7fff207b1cfb
(IMP) $0 = 0x00007fff207b1cfb (Foundation`_NSSetIntValueAndNotify)

// person2
p (IMP) 0x102539e70
(IMP) $1 = 0x0000000102539e70 (OC对象的本质`-[Person setAge:] at Person.h:12)

如上:我们发现person1setAge:方法其实是调用到了一个C函数:_NSSetIntValueAndNotify

通过上面的分析:

1、我们看到person1添加了KVO之后,其isa 指针指向的是一个派生类NSKVONotifying_Person,这个类是Runtime在程序运行的过程中动态创建的一个类,这个类继承自Person

2、在这个派生类里面调用了C函数:_NSSetIntValueAndNotify

3、在_NSSetIntValueAndNotify里面实现如下代码:

// 伪代码
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];

4、在didChangeValueForKey方法里面去通知监听器某个属性值发生了改变。

用一张图来做一个总结

未添加KVO监听的对象:

1

使用KVO添加监听的对象:

2

注意

通过Runtime中的object_class拿到的class对象才是真正的class对象,通过class拿到的不一定是真正的class对象,比如使用了KVO监听的对象。

如何验证派生类NSKVONotifing_Person重写了哪些方法?

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

Class cls = object_getClass(self.person1);
[self printMethodNameOfClass:cls];

- (void)printMethodNameOfClass:(Class)cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSLog(@"%@",NSStringFromSelector(method_getName(method)));
}
free(methodList);
}

打印结果:

2022-11-16 13:36:32.484809+0800 OC对象的本质[26389:16561796] setAge:
2022-11-16 13:36:32.484965+0800 OC对象的本质[26389:16561796] class
2022-11-16 13:36:32.485084+0800 OC对象的本质[26389:16561796] dealloc
2022-11-16 13:36:32.485195+0800 OC对象的本质[26389:16561796] _isKVOA

从上面打印可以看到:NSKVONotifinh_Person类重写了setAge:classdealloc_isKVOA方法。

KVO的触发场景

从上面KVO的本质分析可以看到:只要有setter方法就可以通过KVO来监听值的改变,比如:属性值发生改变、通过KVC赋值。

请看下面场景:

// Person类
@interface Person : NSObject
{
@public
int age;
}
@end

@implementation Person

@end

// KVOController
@interface KVOViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation KVOViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.person->age = 10;

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 不会触发KVO
// [self.person willChangeValueForKey:@"age"];
self.person->age = 11;
// [self.person didChangeValueForKey:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@对象的%@属性发生了改变:\n%@",object, keyPath,change);
}

- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}

@end

如上:

通过self.person->age = 11不会触发KVO,原因相信大家都很清楚了,没有调用setter,可以在self.person->age = 11前后分别添加[self.person willChangeValueForKey:@"age"][self.person didChangeValueForKey:@"age"]来手动触发KVO。

也可以通过KVC赋值[self.person setValue:@11 forKey:@"age"],这样就可以自动触发KVO。

其他补充

如何查看某个方法的函数地址(IMP)?

- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;