Runtime之KVO

Runtime

KVO(Key-Value-Observer)即键值监听,我们在平时的开发中通常用来监听对象属性的变化,比如UIScrollViewContentOffset,不过同时我们要注意在不需要继续监听的时候及时的移除监听,否则可能会导致崩溃。因此这篇文章让我们更好的了解KVO

KVO的使用

下面我们来看下我们平时使用KVO的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface ViewController ()
@property (nonatomic, strong) Person *man;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.man = [[Person alloc] init];
[self addKVO];
}

- (void)addKVO {
[self.man addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

- (void)removeKVO {
[self.man removeObserver:self forKeyPath:@"name"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
self.man.name = @"LeeWong";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqual:@"name"]) {
NSLog(@"keyPath %@ object %@ change%@ context %@",keyPath,object,change,context);
}
}

@end

每次点击屏幕控制台打印如下:

1
2
3
4
5
2020-12-06 20:05:15.531233+0800 Runtime-KVO[2252:11056787] keyPath name object <Person: 0x600001028060> change{
kind = 1;
new = LeeWong;
old = "<null>";
} context self

KVO的使用主要有下面三步:

添加监听

我们可以通过调用addObserver方法添加对某个对象的某个属性进行监听,同时我们还可以设置什么情况下会触发我们的监听,这里有一个枚举:

1
2
3
4
5
6
7
8
9
10
11
12
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
// 属性发生改变时是否需要提供属性的新值
NSKeyValueObservingOptionNew = 0x01,
// 属性发生改变时是否需要提供属性的旧值
NSKeyValueObservingOptionOld = 0x02,
// 如果指定,则在添加观察者的时候立即发送一个通知给观察者 并且是在注册观察者方法返回之前
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
// 并且是在注册观察者方法返回之前这与-willChangeValueForKey:被触发的时间是相对应的
// 这样,在每次修改属性时,实际上是会发送两条通知
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08

};

对于context这个字段实际上我们可以看做给我们提供了一个在观察者与被观察者之间传值的属性,当多个对象监听同一个对象的某个属性发生改变时可用来区分来源。

监听改变

当被监听的属性发生改变时,会触发observeValueForKeyPath方法

这个方法共有四个参数:

keyPath

被监听的属性

object

被监听的属性所属的对象

change

本次监听的改变 具体值与添加监听者时NSKeyValueObservingOptions设置有关

在上面的例子中change对应的值为:

1
2
3
4
5
change{
kind = 1;
new = LeeWong;
old = "<null>";
}

我们来看下change中对应信息的key值有哪些分别用来获取哪些value值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 属性变化的类型,是一个NSNumber对象,包含NSKeyValueChange枚举相关的值
NSString *const NSKeyValueChangeKindKey;

// 属性的新值。当NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加观察的方法设置了NSKeyValueObservingOptionNew时,我们能获取到属性的新值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionNew时,则我们能获取到一个NSArray对象,包含被插入的对象或
// 用于替换其它对象的对象。
NSString *const NSKeyValueChangeNewKey;

// 属性的旧值。当NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加观察的方法设置了NSKeyValueObservingOptionOld时,我们能获取到属性的旧值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionOld时,则我们能获取到一个NSArray对象,包含被移除的对象或
// 被替换的对象。
NSString *const NSKeyValueChangeOldKey;

// 如果NSKeyValueChangeKindKey的值是NSKeyValueChangeInsertion、NSKeyValueChangeRemoval
// 或者NSKeyValueChangeReplacement,则这个key对应的值是一个NSIndexSet对象,
// 包含了被插入、移除或替换的对象的索引
NSString *const NSKeyValueChangeIndexesKey;

// 当指定了NSKeyValueObservingOptionPrior选项时,在属性被修改的通知发送前,
// 会先发送一条通知给观察者。我们可以使用NSKeyValueChangeNotificationIsPriorKey
// 来获取到通知是否是预先发送的,如果是,获取到的值总是@(YES)
NSString *const NSKeyValueChangeNotificationIsPriorKey;

同时,NSKeyValueChangeKindKey对应的枚举有:

1
2
3
4
5
6
7
8
9
10
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
//设置一个新值。被监听的属性可以是一个对象,也可以是一对一关系的属性或一对多关系的属性。
NSKeyValueChangeSetting = 1,
//表示一个对象被插入到一对多关系的属性。
NSKeyValueChangeInsertion = 2,
// 表示一个对象被从一对多关系的属性中移除。
NSKeyValueChangeRemoval = 3,
// 表示一个对象在一对多的关系的属性中被替换
NSKeyValueChangeReplacement = 4,
};

context

额外数据

移除监听 removeObserver

当监听者不需要监听变化时,需要调用removeObserver方法移除监听。需要注意的是,在监听者被释放前,必须要调用removeObserver:forKeyPath:将其移除,否则会crash。我们看下下面这段代码:

1
2
3
4
5
6
- (void)addKVOCrashTest {
Person *person = [Person new];
Person *person2 = [Person new];
[person2 addObserver:person forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"person"];
person2.name = @"LeeWong";
}

很明显,上述代码在person释放时并没有移除监听,因此控制台打印如下:

1
2
3
4
5
6
7
8
9
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<Person: 0x6000018fc120>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: name
Observed object: <Person: 0x6000018fc100>
Change: {
kind = 1;
new = LeeWong;
old = "<null>";
}

通过上面的描述我们对KVO的使用有了一个基本的了解,下面我们来看下KVO相关的干货。

KVO 如何实现

KVO 如果要你去实现 你会如何实现?

我们可以先来思考下这个问题,KVO实际上就是监听属性改变,我们可能最先想到的就是重写setter方法,在setter方法被调用时我们额外调用一个代理方法通知外部,如果不如想监听属性的改变,添加代理监听就可以了,而且在setter方法中我们可以拿到对应的新值或者旧值,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13

@protocol PersonKVODelegate <NSObject>
@optional
- (void)personObjectNamePropertyChangeFrom:(NSString *)oldName newName:(NSString *)newName;

@end

- (void)setName:(NSString *)name {
if (self.delegate && [self.delegate respondsToSelector:@selector(personObjectNamePropertyChangeFrom:newName:)]) {
[self.delegate personObjectNamePropertyChangeFrom:_name newName:name];
}
_name = name;
}

这样外部在想监听值改变的时候先设置代理,然后实现对应的方法就可以了。那么系统到底是怎么实现的呢?我们下面来一探究竟。

KVO 实现

在我们查看KVO监听时我们看到下面这段代码注释:

1
2
3
4
5
6
7
8
9
// 对于任意类型的属性 NSKeyValueChangeSetting 表示被监听的对象接收到-setValue:forKey:
// 消息调用或对象的setter方法被调用或者
// -willChangeValueForKey:/-didChangeValueForKey: 被调用
- For any sort of property (attribute, to-one relationship, or ordered or unordered to-many relationship) NSKeyValueChangeSetting indicates that the observed object has received a -setValue:forKey: message, or that the key-value coding-compliant set method for the key has been invoked, or that a -willChangeValueForKey:/-didChangeValueForKey: pair has otherwise been invoked.

// 对于一个有序一对多关系,NSKeyValueChangeInsertion、NSKeyValueChangeRemoval NSKeyValueChangeReplacement 表示通过调用对象的mutableArrayValueForKey:方法给array发送了一个修改消息或者数组或有序集合的修改方法被调用再或者-willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey 被调用
- For an _ordered_ to-many relationship, NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, and NSKeyValueChangeReplacement indicate that a mutating message has been sent to the array returned by a -mutableArrayValueForKey: message sent to the object, or sent to the ordered set returned by a -mutableOrderedSetValueForKey: message sent to the object, or that one of the key-value coding-compliant array or ordered set mutation methods for the key has been invoked, or that a -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey: pair has otherwise been invoked.
// 对于一个无序的一对多关系 NSKeyValueChangeInsertion NSKeyValueChangeRemoval NSKeyValueChangeReplacement 表示通过调用对象mutableSetValueForKey方法,导致集合的修改方法被调用,或者key-value coding-compliant set 修改方法被调用,或者willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: 方法被调用
- For an _unordered_ to-many relationship (introduced in Mac OS 10.4), NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, and NSKeyValueChangeReplacement indicate that a mutating message has been sent to the set returned by a -mutableSetValueForKey: message sent to the object, or that one of the key-value coding-compliant set mutation methods for the key has been invoked, or that a -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: pair has otherwise been invoked.

我们看到无论是一对一还是一对多或者有序还是无序触发的原因之一都有willChangeValueForKeydidChangeValueForKey这两个方法,那么这两个方法是做什么的呢?

NSObject(NSKeyValueObserverNotification)

我们在NSKeyValueObserving.h文件中,看到了NSObject(NSKeyValueObserverNotification)分类,在这个分类中,我们我们看到上面提到的几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface NSObject(NSKeyValueObserverNotification)
// 通知外部对象的某个属性改变
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

// 有序的一对多的集合 通知外部属性发生了改变
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

// 无需的一对多的结合 通知外部属性发生了改变
- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;

@end

我们发现分类中的几个方法和上面我们提到的被调用的方法是一一对应的。

我们先简单看下前两个方法

1
2
3
4
5
6
7
8
/* Given a key that identifies a property (attribute, to-one relationship, or ordered or unordered to-many relationship), send -observeValueForKeyPath:ofObject:change:context: notification messages of kind NSKeyValueChangeSetting to each observer registered for the key, including those that are registered with other objects using key paths that locate the keyed value in this object. Invocations of these methods must always be paired.

The change dictionaries in notifications resulting from use of these methods contain optional entries if requested at observer registration time:
- The NSKeyValueChangeOldKey entry, if present, contains the value returned by -valueForKey: at the instant that -willChangeValueForKey: is invoked (or an NSNull if -valueForKey: returns nil).
- The NSKeyValueChangeNewKey entry, if present, contains the value returned by -valueForKey: at the instant that -didChangeValueForKey: is invoked (or an NSNull if -valueForKey: returns nil).
*/
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

给定一个标识属性的键(属性,一对一关系或有序或无序多对关系),向每个注册该键的观察者发送-observeValueForKeyPath:ofObject:change:context:类型为NSKeyValueChangeSetting的通知消息。这些方法的调用必须始终配对。

如果注册监听的时候有添加,改变通知的字典中会包含下面这两个属性:
NSKeyValueChangeOldKey:旧值,NSKeyValueChangeNewKey 新值

简单一点去理解就是这两个方法被调用时会触发调用observeValueForKeyPath:ofObject:change:context: 方法

既然在属性改变的时候回调用这两个方法,我们是否可以通过重写这两个方法来控制KVO的调用呢?或者什么时间我们需要重写呢?

我们先重写这两个方法,然后看下调用时机:

1
2
3
4
5
6
7
8
9
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"Person willChangeValueForKey %@",key);
}

- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
NSLog(@"Person didChangeValueForKey %@",key);
}

控制台输出结果:

1
2
3
4
5
6
7
2020-12-12 12:39:40.433214+0800 Runtime-KVO[52130:15055464] Person willChangeValueForKey name
2020-12-12 12:39:40.433527+0800 Runtime-KVO[52130:15055464] keyPath name object <Person: 0x600002f65740> change{
kind = 1;
new = LeeWong;
old = "<null>";
} context self
2020-12-12 12:39:40.433643+0800 Runtime-KVO[52130:15055464] Person didChangeValueForKey name

这里我们可以明显的看出willChangeValueForKeydidChangeValueForKey的调用时机。因此我们猜测其具体的实现应该是:

1
2
3
4
5
- (void)setName:(NSString *)name {
[self willChangeValueForKey:name];
_name = name;
[self didChangeValueForKey:name];
}

上面是我们从方法调用层看到的KVO的实现,下面我们从底层分析下KVO具体是如何实现的。

KVO 底层实现

我们先从官方文档中看下官方对KVO的解释

1
2
3
4
5
6
7
Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

自动的键值监听是通过使用isa-swizzling技术实现,即isa交换,下面我们重点看下这段话:

1
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class.

当注册了某各类的属性监听后,isa指针指向的实际上是另一个中间类而不是真正的类。

我们先通过下面这个例子来验证一下这个说法:

1
2
3
NSLog(@"class %@",NSStringFromClass([self.man class]));

NSLog(@"isa class %@",object_getClass(self.man));

打印结果:

1
2
2020-12-12 13:36:28.533095+0800 Runtime-KVO[55411:15105963] class Person
2020-12-12 13:36:28.533274+0800 Runtime-KVO[55411:15105963] isa class NSKVONotifying_Person

果然我们通过Class方法和object_getClass获取到的类是不同的,这也验证了实际上系统对有监听的类实际上实现了一个中间类,那么如果我们移除了监听呢?

下面我们来看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)kvoClassChange {
[self printClassInfo];
[self addKVO];
[self printClassInfo];
[self removeKVO];
[self printClassInfo];
}

- (void)printClassInfo {
NSLog(@"---------------------------------");
NSLog(@"class %@",NSStringFromClass([self.man class]));
NSLog(@"isa class %@",object_getClass(self.man));
}

- (void)addKVO {
[self.man addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"self"];
}

- (void)removeKVO {
[self.man removeObserver:self forKeyPath:@"name"];
}

我们看下打印结果:

1
2
3
4
5
6
7
8
9
2020-12-12 14:10:07.443251+0800 Runtime-KVO[57507:15138615] ---------------------------------
2020-12-12 14:10:07.443362+0800 Runtime-KVO[57507:15138615] class Person
2020-12-12 14:10:07.443446+0800 Runtime-KVO[57507:15138615] isa class Person
2020-12-12 14:10:07.443787+0800 Runtime-KVO[57507:15138615] ---------------------------------
2020-12-12 14:10:07.443896+0800 Runtime-KVO[57507:15138615] class Person
2020-12-12 14:10:07.443988+0800 Runtime-KVO[57507:15138615] isa class NSKVONotifying_Person
2020-12-12 14:10:07.444116+0800 Runtime-KVO[57507:15138615] ---------------------------------
2020-12-12 14:10:07.444209+0800 Runtime-KVO[57507:15138615] class Person
2020-12-12 14:10:07.444303+0800 Runtime-KVO[57507:15138615] isa class Person

很明显我们发现,在我们添加KVO监听之后和移除KVO监听之前我们的对象的isa指针实际上指向了一个新的类NSKVONotifying_Person,因此我们在判断类型的时候应该使用Class而不是isa指向的class

那么NSKVONotifying_PersonPerson类是什么关系呢?

1
2
3
4
5
6
7
8
9
10
- (void)printKVOClassChain {
NSLog(@"---------------------------------");
Class cls = object_getClass(self.man);
while (cls) {
NSLog(@"claas %@ 's supercls %@",cls,class_getSuperclass(cls));
cls = class_getSuperclass(cls);
}

NSLog(@"---------------------------------");
}

打印结果如下:

1
2
3
2020-12-12 14:38:51.707598+0800 Runtime-KVO[59228:15165470] claas NSKVONotifying_Person 's supercls Person
2020-12-12 14:38:51.707712+0800 Runtime-KVO[59228:15165470] claas Person 's supercls NSObject
2020-12-12 14:38:51.707802+0800 Runtime-KVO[59228:15165470] claas NSObject 's supercls (null)

通过打印结果我们发现实际上NSKVONotifying_PersonPerson的一个子类。

那么NSKVONotifying_Person这个类的方法列表结构:

1
2
3
4
5
6
7
8
9
10
11
- (void)printClassInfo {
NSLog(@"---------------------------------");
Class cls = object_getClass(self.man);
NSLog(@"isa class %@",cls);
NSLog(@"---------------------------class method list ");
NSUInteger methodCount = 0;
Method * methodList = class_copyMethodList(object_getClass(self.man), &methodCount);
for(NSUInteger i = 0; i < methodCount; i++) {
NSLog(@"%@",NSStringFromSelector(method_getName(methodList[i])));
}
}

打印结果:

1
2
3
4
5
6
7
2020-12-12 14:33:57.447079+0800 Runtime-KVO[58941:15160616] ---------------------------------
2020-12-12 14:33:57.447190+0800 Runtime-KVO[58941:15160616] isa class NSKVONotifying_Person
2020-12-12 14:33:57.447283+0800 Runtime-KVO[58941:15160616] ---------------------------class method list
2020-12-12 14:33:57.447388+0800 Runtime-KVO[58941:15160616] setName:
2020-12-12 14:33:57.447481+0800 Runtime-KVO[58941:15160616] class
2020-12-12 14:33:57.447597+0800 Runtime-KVO[58941:15160616] dealloc
2020-12-12 14:33:57.447694+0800 Runtime-KVO[58941:15160616] _isKVOA

上面我们已经知道NSKVONotifying_Person类实际是Person的一个子类:

  • 这里setName我们理解就是重写父类Observer监听属性的setter方法,然后内部调用了didChangeValueForKey:willChangeValueForKey:方法用于通知外部,
  • class 方法重写是为了外部在调用class方法获取类的类型是获取到的是父类的类型
  • dealloc 是为了在父类被释放的时候子类检查是否被释放了
  • _isKVOA 来说明自己是一个KVO

这样我们就了解了KVO的整个实现原理,下面我们来看下KVO的一些扩展用法。

KVO 应用

如何监听数组个数改变

我们上面都是用KVO监听对象的某个属性的改变,那么如果我们要监听集合类型的属性呢?

我们上面了解到了实际上系统是重写了属性的setting方法,然后通知到外部属性改变,但是当我们向数组中添加或者删除元素时,实际上我们并没有修改数组的count属性,那么我们该如何监听count的改变呢?

我们先尝试用正常监听对象属性的方式添加监听:

1
2
3
4
5
- (void)arrayKVO {
self.personArray = [@[@"1"] mutableCopy];
[self.personArray addObserver:self forKeyPath:@"count" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"aaa"];
[self.personArray addObject:@"2"];
}

我们看下输出结果:

1
2
2020-12-12 14:51:30.825912+0800 Runtime-KVO[59915:15176280] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<__NSArrayM 0x60000246f0c0> addObserver:forKeyPath:options:context:] is not supported. Key path: count'
*** First throw call stack:

结果很明显我们无法监听count,系统直接崩溃。那么我们应该如何监听呢? 但是当我监听一个自定义对象中的一个数组属性时却并没有出现crash,这里苦思不得解。我们先来看下这段代码

1
2
3
4
- (void)personChildrenKVO {
[self.man addObserver:self forKeyPath:@"children" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"aaa"];
[self.man.children addObject:@"2"];
}

其中children是Person对象中的一个可变数组属性

1
2
3
@interface Person : NSObject
@property (nonatomic, strong) NSMutableArray *children;
@end

不过虽然没有崩溃但是依然我们无法监听到数组的改变,那么我们究竟如何才可以监听到数组的改变呢?

实际上通过上面对KVO的原理的理解,实际上对于Person对象,我们在KVO监听时,实际上系统实现了KVO的子类并实现了被监听属性的setter方法,然后在内部调用触发KVO的方法,但是很明显这里并没有调用到KVO的监听方法,那么这里对于集合类型的数组和普通的对象是否有区别呢?

我们按照上面的逻辑在进一步的分析一下:

我们修改数组中元素个数实际上是通过给数组增加或者删除元素来实现的,但是根据上面的了解,如果子类对象只是实现了childrensetter方法,在我们添加或者删除元素的时候并不会调用到setter方法,而是调用了addObject:或者removeObject:。这样实际并不会通知到外部。我们在最开始讨论KVO实现的时候,我们看到了对于有序集合提到了一个方法mutableArrayValueForKey: 我们先看下这个方法

1
2
3
4
5
6
7
8
9
10
11
/* Given a key that identifies an _ordered_ to-many relationship, return a mutable array that provides read-write access to the related objects. Objects added to the mutable array will become related to the receiver, and objects removed from the mutable array will become unrelated.

The default implementation of this method recognizes the same simple accessor methods and array accessor methods as -valueForKey:'s, and follows the same direct instance variable access policies, but always returns a mutable collection proxy object instead of the immutable collection that -valueForKey: would return. It also:
1. Searches the class of the receiver for methods whose names match the patterns -insertObject:in<Key>AtIndex: and -removeObjectFrom<Key>AtIndex: (corresponding to the two most primitive methods defined by the NSMutableArray class), and (introduced in Mac OS 10.4) also -insert<Key>:atIndexes: and -remove<Key>AtIndexes: (corresponding to -[NSMutableArray insertObjects:atIndexes:] and -[NSMutableArray removeObjectsAtIndexes:). If at least one insertion method and at least one removal method are found each NSMutableArray message sent to the collection proxy object will result in some combination of -insertObject:in<Key>AtIndex:, -removeObjectFrom<Key>AtIndex:, -insert<Key>:atIndexes:, and -remove<Key>AtIndexes: messages being sent to the original receiver of -mutableArrayValueForKey:. If the class of the receiver also implements an optional method whose name matches the pattern -replaceObjectIn<Key>AtIndex:withObject: or (introduced in Mac OS 10.4) -replace<Key>AtIndexes:with<Key>: that method will be used when appropriate for best performance.
2. Otherwise (no set of array mutation methods is found), searches the class of the receiver for an accessor method whose name matches the pattern -set<Key>:. If such a method is found each NSMutableArray message sent to the collection proxy object will result in a -set<Key>: message being sent to the original receiver of -mutableArrayValueForKey:.
3. Otherwise (no set of array mutation methods or simple accessor method is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key> or <key>, in that order. If such an instance variable is found, each NSMutableArray message sent to the collection proxy object will be forwarded to the instance variable's value, which therefore must typically be an instance of NSMutableArray or a subclass of NSMutableArray.
4. Otherwise (no set of array mutation methods, simple accessor method, or instance variable is found), returns a mutable collection proxy object anyway. Each NSMutableArray message sent to the collection proxy object will result in a -setValue:forUndefinedKey: message being sent to the original receiver of -mutableArrayValueForKey:. The default implementation of -setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.

Performance note: the repetitive -set<Key>: messages implied by step 2's description are a potential performance problem. For better performance implement insertion and removal methods that fulfill the requirements for step 1 in your KVC-compliant class. For best performance implement a replacement method too.
*/
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

我们来看下官方对这个方法的解释:

调用mutableArrayValueForKey后,KVC先会搜索类中是否有insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AtIndexes , remove<Key>AtIndexes 格式的方法,如果至少找到一个insert方法和一个remove方法,那么返回一个可以响应NSMutableArray所有方法的代理集合,那么给这个代理集合发送NSMutableArray的方法,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes组合的形式调用。
如果上述的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set<Key>:方法。

结合我们上面猜测没有触发KVO监听的原因:没有调用到数组的setter方法,在结合上面的解释,我们做下面的尝试:

1
2
3
4
5
6
7
- (void)personChildrenKVO {
NSLog(@"111children class %@ object %p",object_getClass(self.man.children),self.man.children);
[self.man addObserver:self forKeyPath:@"children" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"aaa"];
NSLog(@"222children class %@ object %p",object_getClass(self.man.children),self.man.children);
[[self.man mutableArrayValueForKey:@"children"] addObject:@"2"];
NSLog(@"333children class %@ object %p",object_getClass(self.man.children),self.man.children);
}

我们在看下打印结果:

1
2
3
4
2020-12-13 13:46:35.174397+0800 Runtime-KVO[76796:15662273] 111children class __NSArrayM object 0x6000016cc2a0
2020-12-13 13:46:35.174787+0800 Runtime-KVO[76796:15662273] 222children class __NSArrayM object 0x6000016cc2a0
2020-12-13 13:46:35.175057+0800 Runtime-KVO[76796:15662273] observeValueForKeyPath for keyPath children
2020-12-13 13:46:35.175175+0800 Runtime-KVO[76796:15662273] 333children class __NSArrayM object 0x6000016cccc0

我们发现,我们的KVO生效了。

当我们在代理方法中打了断点后,方法调用的堆栈如下:

这也恰好与我们的猜测相对应:我们发现在监听被触发后,我们的children数组的地址也发生了变化,这里跟我们猜测相同,这里应该是有一个children的子类(图中的NSKeyValueNotifyingMutableArray),实现了children的某些方法。

在刚才了解mutableArrayValueForKey:方法时,我们看到有一个提示:

1
Performance note: the repetitive -set<Key>: messages implied by step 2's description are a potential performance problem. For better performance implement insertion and removal methods that fulfill the requirements for step 1 in your KVC-compliant class. For best performance implement a replacement method too.

因为子类没有实现而调用set方法可能会有性能问题,因此最好实现insert或者remove方法。

那么我们看下该如何实现:
依据上面描述的,我们在Person类中新增下面几个方法

1
2
3
4
5
6
7
- (instancetype)initWithChildren:(NSMutableArray *)children;

- (void)removeObjectFromChildrenAtIndex:(NSUInteger)index;
- (void)removeChildrenAtIndexes:(NSIndexSet *)indexes;
- (void)insertObject:(id)object inChildrenAtIndex:(NSUInteger)index;
- (void)insertChildren:(NSArray *)array atIndexes:(NSIndexSet *)indexes;

然后在内部实现这几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

@implementation Person

- (instancetype)initWithChildren:(NSMutableArray *)children {
if (self = [super init]) {
_children = children;
}
return self;
}

- (instancetype)init {
return [self initWithChildren:[NSMutableArray array]];
}

- (void)setName:(NSString *)name {
[self willChangeValueForKey:name];
_name = name;
[self didChangeValueForKey:name];
}

- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"Person willChangeValueForKey %@",key);
}

- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
NSLog(@"Person didChangeValueForKey %@",key);
}


- (void)insertChildren:(NSArray *)array atIndexes:(NSIndexSet *)indexes {
[self.children insertObjects:array atIndexes:indexes];
}

- (void)insertObject:(id)object inChildrenAtIndex:(NSUInteger)index {
[self.children insertObject:object atIndex:index];
}

- (void)removeChildrenAtIndexes:(NSIndexSet *)indexes {
[self.children removeObjectsAtIndexes:indexes];
}

- (void)removeObjectFromChildrenAtIndex:(NSUInteger)index {
[self.children removeObjectAtIndex:index];
}

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key {
[super willChange:changeKind valuesAtIndexes:indexes forKey:key];
NSLog(@"Person willChange:changeKind %@ valuesAtIndexes:indexes= %@ forKey:key = %@",@(changeKind),indexes,key);
}

- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key {
[super didChange:changeKind valuesAtIndexes:indexes forKey:key];
NSLog(@"Person didChange:changeKind %@ valuesAtIndexes:indexes= %@ forKey:key = %@",@(changeKind),indexes,key);
}

然后我们看下调用的位置:

1
2
3
4
5
6
7
- (void)personChildrenKVO {
NSLog(@"111children class %@ object %p",object_getClass(self.man),self.man);
[self.man addObserver:self forKeyPath:@"children" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"aaa"];
NSLog(@"222children class %@ object %p",object_getClass(self.man),self.man);
[self.man insertObject:@"111" inChildrenAtIndex:0];
NSLog(@"333children class %@ object %p",object_getClass(self.man),self.man);
}

打印结果:

1
2
3
4
5
6
2020-12-13 14:59:04.047625+0800 Runtime-KVO[78041:15718979] 111children class Person object 0x600000455340
2020-12-13 14:59:04.047936+0800 Runtime-KVO[78041:15718979] 222children class NSKVONotifying_Person object 0x600000455340
2020-12-13 14:59:04.048149+0800 Runtime-KVO[78041:15718979] Person willChange:changeKind 2 valuesAtIndexes:indexes= <_NSCachedIndexSet: 0x60000045c5c0>[number of indexes: 1 (in 1 ranges), indexes: (0)] forKey:key = children
2020-12-13 14:59:04.048284+0800 Runtime-KVO[78041:15718979] observeValueForKeyPath for keyPath children
2020-12-13 14:59:04.048412+0800 Runtime-KVO[78041:15718979] Person didChange:changeKind 2 valuesAtIndexes:indexes= <_NSCachedIndexSet: 0x60000045c5c0>[number of indexes: 1 (in 1 ranges), indexes: (0)] forKey:key = children
2020-12-13 14:59:04.048521+0800 Runtime-KVO[78041:15718979] 333children class NSKVONotifying_Person object 0x600000455340

从log中我们看到,通过这种方法我们可以使用KVO捕捉到数组的变化。而且我们发现添加KVO之后我们的Person类也变成了NSKVONotifying_Person类型,同样在KVO被触发的前后willChange:changeKind:valuesAtIndexesdidChange:changeKind:valuesAtIndexes方法也被调用了。进而调用KVO的代理方法通知外部属性改变。

如何监听复合改变

在我们平时的工作中除了监听对象的某个属性的改变,还可能会用到监听某些复合条件的值的改变,尤其是之前用到RAC时,我们可以绑定监听2个属性的信号,两个监听的对象中中任何一个发生改变都需要通知到监听者,这种如果使用KVO该如何实现呢?

例如还是在我们Person对象中,我们有两个属性:姓名和年龄,我们还有一个方法用来获取这个人的简介

1
2
3
- (NSString *)personDescription {
return [NSString stringWithFormat:@"My name is %@,I'm %@ years old",self.name,@(self.age)];
}

我们希望监听personDescription的改变,但是personDescription的改变实际上依赖nameage的改变。

这种情况下实际上系统也为我们提供了一个方法:

1
2
3
4
5
6
7
8
9
10

/* Return a set of key paths for properties whose values affect the value of the keyed property. When an observer for the key is registered with an instance of the receiving class, KVO itself automatically observes all of the key paths for the same instance, and sends change notifications for the key to the observer when the value for any of those key paths changes. The default implementation of this method searches the receiving class for a method whose name matches the pattern +keyPathsForValuesAffecting<Key>, and returns the result of invoking that method if it is found. So, any such method must return an NSSet too. If no such method is found, an NSSet that is computed from information provided by previous invocations of the now-deprecated +setKeys:triggerChangeNotificationsForDependentKey: method is returned, for backward binary compatibility.

This method and KVO's automatic use of it comprise a dependency mechanism that you can use instead of sending -willChangeValueForKey:/-didChangeValueForKey: messages for dependent, computed, properties.

You can override this method when the getter method of one of your properties computes a value to return using the values of other properties, including those that are located by key paths. Your override should typically invoke super and return a set that includes any members in the set that result from doing that (so as not to interfere with overrides of this method in superclasses).

You can't really override this method when you add a computed property to an existing class using a category, because you're not supposed to override methods in categories. In that case, implement a matching +keyPathsForValuesAffecting<Key> to take advantage of this mechanism.
*/
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

这个方法返回一个keypath集合,集合中的keyPath值的改变会影响到关键属性(key)的值,当有观察者被注册监听关键属性(key)时,KVO自身会自动观察这个对象的所有keyPath,当其中的任意一个发生改变时都会通知到注册监听的观察者。这个机制的默认实现是搜索接受者的方法列表查看是否有方法名匹配keyPathsForValuesAffecting的方法,通过调用这个方法返回一个属性集合。

下面我们来尝试实现下这个方法看看有何效果:

1
2
3
4
5
6
7
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"personDescription"]) {
keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"name",@"age"]];
}
return keyPaths;
}

我们在项目中添加监听:

1
2
3
4
5
- (void)addPersonInfoDescrption {
[self.man addObserver:self forKeyPath:@"personDescription" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@""];
self.man.name = @"LeeWong";
self.man.age = 30;
}

再来看下控制台的输出:

1
2
3
4
5
6
2020-12-13 20:52:59.084192+0800 Runtime-KVO[81809:15903199] Person willChangeValueForKey name
2020-12-13 20:52:59.084354+0800 Runtime-KVO[81809:15903199] observeValueForKeyPath for keyPath personDescription
2020-12-13 20:52:59.084446+0800 Runtime-KVO[81809:15903199] Person didChangeValueForKey name
2020-12-13 20:52:59.085154+0800 Runtime-KVO[81809:15903199] Person willChangeValueForKey age
2020-12-13 20:52:59.085245+0800 Runtime-KVO[81809:15903199] observeValueForKeyPath for keyPath personDescription
2020-12-13 20:52:59.085350+0800 Runtime-KVO[81809:15903199] Person didChangeValueForKey age

这样我们就实现了复合属性的监听。

如何手动控制KVO

在了解了KVO的机制之后,我们发现我们KVO监听的代理方法被调用频次非常高,尤其是无论是我们监听了多少属性的改变,我们的代理方法只有一个,我们要在其中做很多判断。所以是否有方法可以手动的去控制KVO监听的调用频次。

在上面的介绍中我们知道,代理方法被调用是因为willChangeValueForKeydidChangeValueForKey这类方法被调用,那么我们是否可以控制这些方法被调用的频次呢?

1
2
3
/* Return YES if the key-value observing machinery should automatically invoke -willChangeValueForKey:/-didChangeValueForKey:, -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey:, or -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: whenever instances of the class receive key-value coding messages for the key, or mutating key-value coding-compliant methods for the key are invoked. Return NO otherwise. Starting in Mac OS 10.5, the default implementation of this method searches the receiving class for a method whose name matches the pattern +automaticallyNotifiesObserversOf<Key>, and returns the result of invoking that method if it is found. So, any such method must return BOOL too. If no such method is found YES is returned.
*/
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

这个方法通过返回YES/NO控制是否应该自动调用-willChangeValueForKey:/-didChangeValueForKey:这类方法。当我们返回NO时就不会自动调用上述方法,我们先来尝试下

1
2
3
4
5
6
7
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = [super automaticallyNotifiesObserversForKey:key];
if ([key isEqualToString:@"name"]) {
automatic = NO;
}
return automatic;
}

我们在主工程在此添加对属性name的监听:

1
2
3
4
- (void)addPersonNameKVO {
[self.man addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@""];
self.man.name = @"LeeWong";
}

当我们执行之后控制台没有任何输出,这也证明了我们刚才实现的automaticallyNotifiesObserversForKey生效了,那么我们把系统的调用屏蔽之后需要自己手动调用对应的方法:

1
2
3
4
5
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}

再次运行,控制台输出:

1
2
3
4
5
6
7
8
2020-12-13 20:49:08.814044+0800 Runtime-KVO[81739:15899611] Person willChangeValueForKey name
2020-12-13 20:49:08.814166+0800 Runtime-KVO[81739:15899611] observeValueForKeyPath for keyPath name
2020-12-13 20:49:08.814406+0800 Runtime-KVO[81739:15899611] keyPath name object <Person: 0x60000159e610> change{
kind = 1;
new = LeeWong;
old = "<null>";
} context
2020-12-13 20:49:08.814578+0800 Runtime-KVO[81739:15899611] Person didChangeValueForKey name

这与系统实现的一致,只是调用KVO代理方法是由我们自己手动控制,这样我们就可以控制频率了。

监听信息

在最开始介绍KVO时,我们就提到了注册监听时一定要注意移除监听,那么我们是否可以获取,到所有的监听者的信息呢?

NSObject(NSKeyValueObservingCustomization)分类中我们发现了下面这个方法:

1
2
3
/* Take or return a pointer that identifies information about all of the observers that are registered with the receiver, the options that were used at registration-time, etc. The default implementation of these methods store observation info in a global dictionary keyed by the receivers' pointers. For improved performance, you can override these methods to store the opaque data pointer in an instance variable. Overrides of these methods must not attempt to send Objective-C messages to the passed-in observation info, including -retain and -release.
*/
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

下面我们来看下其中具体包含了哪些信息:

1
2
3
4
5
6
- (void)printKVOObserverInfo {
[self.man addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@""];
[self.man addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@""];
id info = self.man.observationInfo;
NSLog(@"%@", [info description]);
}

下面我们看下通过断点看到的效果:

我们看到观察者实际上是一个NSKeyValueObservance对象,对象中包含观察者(observer)、监听的属性(property)。但是NSKeyValueObservance类是一个私有类,我们无法查看这个类的结构,不过在这篇文章作者通过dump的方法获取到类的具体结构,感兴趣的可以进一步了解。

既然我们能通过这种方式获取到某个对象都有哪些监听者,那么我们在添加或者删除监听者时都可以根据要添加或者要删除的监听者是否已经在监听者列表中了。这样就可以避免重复添加或者删除监听者,但是需要注意,这个类是私有类因此属性和方法也是私有的。虽然我们可以通过KVC获取到,但是也面临可能出现的各类问题。

总结

在这篇文章中,我们首先从KVO的添加移除以及代理方法的介绍这几个方法中,让我们对KVO有一个初步的了解,然后我们在从实现的角度去看KVO的实现原理。最后,我们谈到了对于数组等集合类型的监听、对于符合类型的监听、以及如何手动控制KVO的触发。最后我们还介绍了如何获取某个对象KVO监听者列表。相信经过这篇文章,你应该对KVO有了一个更加全面的认知和更深层次的了解。

参考

NSKeyValueObserving(KVO)

Key-Value Observing Programming Guide

Key-Value Observing源码初探

本文demo