探究KVO底层实现

本文最后更新于:2 年前

本文参考《iOS窥探KVO底层实现原理篇》

一、定义

KVO全称为Key Value Observing,键值观察机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法

二、方法

1
2
3
4
5
6
/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
在相对于接收者的关键路径上注册或注销该值的观察者。 这些选项确定观察者通知中包括的内容以及何时发送它们,如上所述,并且上下文如上所述在观察者通知中传递上下文。 您应尽可能使用removeObserver:forKeyPath:context:而不是removeObserver:forKeyPath:,因为它可以让您更精确地指定意图。 当同一观察者多次注册相同的KeyPath路径,但每次都使用不同的上下文指针时,removeObserver:forKeyPath:在确定要删除的对象时必须猜测上下文指针,并且可能会猜错。
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Options for use with -addObserver:forKeyPath:options:context: and -addObserver:toObjectsAtIndexes:forKeyPath:options:context:.
*/
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {

/* Whether the change dictionaries sent in notifications should contain NSKeyValueChangeNewKey and NSKeyValueChangeOldKey entries, respectively.
*/
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,

/* Whether a notification should be sent to the observer immediately, before the observer registration method even returns. The change dictionary in the notification will always contain an NSKeyValueChangeNewKey entry if NSKeyValueObservingOptionNew is also specified but will never contain an NSKeyValueChangeOldKey entry. (In an initial notification the current value of the observed property may be old, but it's new to the observer.) You can use this option instead of explicitly invoking, at the same time, code that is also invoked by the observer's -observeValueForKeyPath:ofObject:change:context: method. When this option is used with -addObserver:toObjectsAtIndexes:forKeyPath:options:context: a notification will be sent for each indexed object to which the observer is being added.
在观察者注册方法返回之前是否立即将通知发送给观察者。 如果还指定了NSKeyValueObservingOptionNew,则通知中的更改字典将始终包含NSKeyValueChangeNewKey条目,但绝不包含NSKeyValueChangeOldKey条目。 (在初始通知中,observed属性的当前值可能是旧的,但对于观察者来说是新的。)您可以使用此选项,而不是同时显式调用观察者的observeValueForKeyPath:ofObject:change:context:方法。 与addObserver:toObjectsAtIndexes:forKeyPath:options:context一起使用此选项时,将向要添加观察者的每个索引对象发送通知。
*/
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,

/* Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change. The change dictionary in a notification sent before a change always contains an NSKeyValueChangeNotificationIsPriorKey entry whose value is [NSNumber numberWithBool:YES], but never contains an NSKeyValueChangeNewKey entry. You can use this option when the observer's own KVO-compliance requires it to invoke one of the -willChange... methods for one of its own properties, and the value of that property depends on the value of the observed object's property. (In that situation it's too late to easily invoke -willChange... properly in response to receiving an -observeValueForKeyPath:ofObject:change:context: message after the change.)

When this option is specified, the change dictionary in a notification sent after a change contains the same entries that it would contain if this option were not specified, except for ordered unique to-many relationships represented by NSOrderedSets. For those, for NSKeyValueChangeInsertion and NSKeyValueChangeReplacement changes, the change dictionary for a will-change notification contains an NSKeyValueChangeIndexesKey (and NSKeyValueChangeOldKey in the case of Replacement where the NSKeyValueObservingOptionOld option was specified at registration time) which give the indexes (and objects) which *may* be changed by the operation. The second notification, after the change, contains entries reporting what did actually change. For NSKeyValueChangeRemoval changes, removals by index are precise.
是否应在每次更改之前和之后将单独的通知发送给观察者,而不是在更改之后将单个通知发送给观察者。更改之前发送的通知中的更改字典始终包含NSKeyValueChangeNotificationIsPriorKey条目,其值为[NSNumber numberWithBool:YES],但从不包含NSKeyValueChangeNewKey条目。当观察者自己的KVO兼容性要求它为其自身的属性之一调用willChange ...方法之一时,可以使用此选项,并且该属性的值取决于所观察对象的属性的值。 (在那种情况下,为响应更改后收到的observeValueForKeyPath:ofObject:change:context:消息,正确地轻松调用willChange ...为时已晚。)

指定此选项后,更改后发送的通知中的更改字典包含与未指定此选项时将包含的条目相同的条目,但NSOrderedSets表示的有序唯一对多关系除外。对于这些更改,对于NSKeyValueChangeInsertion和NSKeyValueChangeReplacement更改,遗嘱变更通知的更改字典包含NSKeyValueChangeIndexesKey(在替换的情况下为NSKeyValueChangeChangeOldKey,在注册时指定了NSKeyValueObservingOptionOld选项的情况下),其索引(和对象)可以* *通过操作更改。更改后的第二个通知包含报告实际更改内容的条目。对于NSKeyValueChangeRemoval更改,按索引的删除是精确的。
*/
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08

};
1
2
3
4
5
6
7
8
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

三、代码测试

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
#import "ViewController.h"
#import "AObject.h"
#import <objc/runtime.h>

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

AObject *a1 = [[AObject alloc] init];
AObject *a2 = [[AObject alloc] init];

NSLog(@"内存:%p,%p", a1, a2);
NSLog(@"类:%@,%@", a1.class, a2.class);
NSLog(@"isa:%@ %@", object_getClass(a1), object_getClass(a2));

SEL sel = @selector(setP:);
IMP imp1 = [a1 methodForSelector:sel];
IMP imp2 = [a2 methodForSelector:sel];
NSLog(@"IMP指针:%p %p", imp1, imp2);

[a1 addObserver:self forKeyPath:@"p" options:(NSKeyValueObservingOptionNew) context:nil];

NSLog(@"添加KVO后内存:%p,%p", a1, a2);
NSLog(@"添加KVO后类:%@,%@", a1.class, a2.class);
NSLog(@"添加KVO后isa:%@ %@", object_getClass(a1), object_getClass(a2));

imp1 = [a1 methodForSelector:sel];
imp2 = [a2 methodForSelector:sel];
NSLog(@"添加KVO后IMP指针:%p %p", imp1, imp2);

a1.p = @"1";
a2.p = @"2";
}

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

@end

2019-12-06 13:56:30.457732+0800 testKVO[854:62432] 内存:0x283a0cc700x283a0cc90
2019-12-06 13:56:30.457797+0800 testKVO[854:62432] 类:AObject,AObject
2019-12-06 13:56:30.457821+0800 testKVO[854:62432] isa:AObject AObject
2019-12-06 13:56:30.457839+0800 testKVO[854:62432] IMP指针:0x1009e6150 0x1009e6150
2019-12-06 13:56:30.458057+0800 testKVO[854:62432] 添加KVO后内存:0x283a0cc700x283a0cc90
2019-12-06 13:56:30.458084+0800 testKVO[854:62432] 添加KVO后类:AObject,AObject
2019-12-06 13:56:30.458106+0800 testKVO[854:62432] 添加KVO后isa:NSKVONotifying_AObject AObject
2019-12-06 13:56:30.458124+0800 testKVO[854:62432] 添加KVO后IMP指针:0x1842b9020 0x1009e6150
  • 添加了KVO之后a1修改p值之后 不再调用AObjectsetP:方法 ,而 a2没有添加监听,依然正常调用setP:方法,由此可以得出系统修改了默认方法实现
1
2
3
4
5
    NSLog(@"添加KVO后isa:%@ %@", object_getClass(a1), object_getClass(a2));
NSLog(@"添加KVO后superclass:%@ %@", class_getSuperclass(object_getClass(a1)), class_getSuperclass(object_getClass(a2)));

2019-12-06 14:04:00.196164+0800 testKVO[881:64017] 添加KVO后isa:NSKVONotifying_AObject AObject
2019-12-06 14:04:00.196194+0800 testKVO[881:64017] 添加KVO后superclass:AObject NSObject
  • NSKVONotifying_AObjectn是苹果动态创建的AObject子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    NSString *methods1 = [self printMethods:object_getClass(a1)];
NSString *methods2 = [self printMethods:object_getClass(a2)];
NSLog(@"%@", methods1);
NSLog(@"%@", methods2);

2019-12-06 14:08:10.447603+0800 testKVO[908:65143] [
setP:
class
dealloc
_isKVOA
]
2019-12-06 14:08:10.447618+0800 testKVO[908:65143] [
.cxx_destruct
p
setP:
]

从输出结果可以看出来NSKVONotifying_AObject内部也有一个setP:方法 还重写了classdealloc方法,由此,给出如下猜想:

  • 添加KVO之后,动态创建了一个子类,继承所观察的类,命名NSKVONotifying_*,重写了所观察属性的set方法,并且通过重写class方法隐藏了子类的存在,在dealloc中进行了释放等操作
  • 属性值发生变化时,触发了NSKVONotifying_*重写的set方法,此时便可以得到原值和新值,并在赋值时触发给定的相应方法
  • 这就能解释:如果观察的属性不存在set方法(直接创建的实例),那么KVO观察就不会生效