0%

KVO本质分析

问题

  • iOS中如何实现对一个对象的KVO?(KVO的本质是什么?)
    答: 当给一个对象添加KVO监听之后,iOS系统会修改这个对象的isa指针,将这个指针指向Runtime动态创建的这个对象的子类。子类拥有自己的set方法的实现,set方法内部会顺序调用willChangeValueForKey方法、父类的set方法、didChangeValueForKey方法,这样会触发observeValueForKeyPath:ofObject:change:context:监听方法。
  • 如何手动触发KVO?
    答: 被监听的属性值被修改时,就会触发KVO。如果想要手动触发KVO,则需要我们手动同时调用willChangeValueForKey方法和didChangeValueForKey方法,这两个方法缺一不可。
  • KVO执行的条件?
    答: 对象添加观察者(addObserver:forKeyPath:)、修改对象被监听的属性、对象在dealloc方法中移除观察者。

官方文档:

Key-Value Observing Implementation Details

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.
大致意思是:KVO使用isa-swizzling技术实现,对象添加观察者之后,isa指针会指向一个临时的类,不能依赖isa的值来判断类的关系,应该使用class方法来判断实例对象真实的类。
接下来使用代码来验证KVO的执行过程:
Person.h (Person.m中什么也没有)

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, assign) int age;

@end

NS_ASSUME_NONNULL_END

ViewController.m (ViewController.h中什么也没有)

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
57
58
59
60
61
62
63
#import "ViewController.h"
#import <objc/runtime.h>
#import "Person.h"

@interface ViewController ()
@property (nonatomic, strong) Person *p1;
@end

@implementation ViewController

- (void)dealloc {
// 移除观察者
[self.p1 removeObserver:self forKeyPath:@"age"];
}


- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

Person *p1 = [[Person alloc] init];
[self printMethods:object_getClass(p1)];
self.p1 = p1;
// 打印p1对象所属类的内部方法列表
NSLog(@"p1 class = %@", [p1 class]);

// 添加观察者
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];

// 打印p1对象所属类的内部方法列表
[self printMethods:object_getClass(p1)];
NSLog(@"p1 class = %@", [p1 class]);
}

/** 打印类中所有的方法 */
- (void) printMethods:(Class)cls
{
unsigned int count ;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];

for (int i = 0 ; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));

[methodNames appendString: methodName];
[methodNames appendString:@" "];

}

NSLog(@"%@",methodNames);
free(methods);
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"执行了监听方法:%@", change);
}

@end

运行结果:

1
2
3
4
2019-03-31 19:06:48.896565+0800 KVO测试Demo[29498:3594134] Person - setAge: age
2019-03-31 19:06:48.897033+0800 KVO测试Demo[29498:3594134] p1 class = Person
2019-03-31 19:06:48.897503+0800 KVO测试Demo[29498:3594134] NSKVONotifying_Person - setAge: class dealloc _isKVOA
2019-03-31 19:06:48.897638+0800 KVO测试Demo[29498:3594134] p1 class = Person

由此可见,对象p1添加观察者之后,p1的所属类由原来的Person类变成了NSKVONotifying_Person类,原来的Person类内部只实现了setAge:方法和age方法,新类NSKVONotifying_Person中有setAge:、class、dealloc、_isKVOA方法。经过分析判断,NSKVONotifying_Person是Person的子类,它重写了父类的setAge:和class方法,使NSKVONotifying_Person的class方法的返回值也是Person,隐藏了内部的实现。

如何手动触发KVO?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

Person *p1 = [[Person alloc] init];
[self printMethods:object_getClass(p1)];
self.p1 = p1;
// 打印p1对象所属类的内部方法列表
NSLog(@"p1 class = %@", [p1 class]);

// 添加观察者
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];

// 打印p1对象所属类的内部方法列表
[self printMethods:object_getClass(p1)];
NSLog(@"p1 class = %@", [p1 class]);

// 手动触发KVO
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
}

执行结果:

1
2
3
4
5
6
7
8
2019-03-31 19:37:41.217175+0800 KVO测试Demo[29990:3611620] Person - setAge: age
2019-03-31 19:37:41.217409+0800 KVO测试Demo[29990:3611620] p1 class = Person
2019-03-31 19:37:41.217999+0800 KVO测试Demo[29990:3611620] NSKVONotifying_Person - setAge: class dealloc _isKVOA
2019-03-31 19:37:41.218145+0800 KVO测试Demo[29990:3611620] p1 class = Person
2019-03-31 19:37:41.218488+0800 KVO测试Demo[29990:3611620] 执行了监听方法:{
kind = 1;
new = 0;
}

如何关闭自动KVO?
Person.m

1
2
3
4
5
6
7
8
9
10
11
12
#import "Person.h"

@implementation Person

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return YES;
}

@end

当返回值为NO的时候,即使改变p1的值也无法触发KVO,此时要想触发KVO只有手动模式,手动调用willChangeValueForKey和didChangeValueForKey。