KVO使用和底层实现原理

使用

KVO 能帮助我们让视图和模型保持同步。控制器可以观察视图依赖的属性变化。

让我们看一个例子:

我们的模型类有以下三个用来代表颜色百分比的属性:

1
2
3
@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;

依赖的属性
我们需要从这个类创建一个 UIColor 对象来显示出颜色。我们添加三个额外的属性,分别对应 R, G, B:

1
2
3
4
@property (nonatomic, readonly) double redComponent;
@property (nonatomic, readonly) double greenComponent;
@property (nonatomic, readonly) double blueComponent;
@property (nonatomic, strong, readonly) UIColor *color;

有了这些以后,我们就可以创建这个类的接口了:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface LabColor : NSObject

@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;

@property (nonatomic, readonly) double redComponent;
@property (nonatomic, readonly) double greenComponent;
@property (nonatomic, readonly) double blueComponent;

@property (nonatomic, strong, readonly) UIColor *color;

@end

这些代码没什么令人激动的地方。有趣的是 greenComponent 属性依赖于 lComponent 和 aComponent。不论何时设置 lComponent 的值,我们需要让 RGB 三个 component 中与其相关的成员以及 color 属性都要得到通知以保持一致。这一点这在 KVO 中很重要。

Foundation 框架提供的表示属性依赖的机制如下:

1
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key

更详细的如下:

1
+ (NSSet *)keyPathsForValuesAffecting<键名>

在我们的例子中如下:

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
//redComponent属性依赖于lComponent属性,如果lComponent改变redComponent属性也随之更改
+ (NSSet *)keyPathsForValuesAffectingRedComponent
{
return [NSSet setWithObject:@"lComponent"];
}
- (double)redComponent;
{
return self.lComponent*255.0;
}
//greenComponent属性依赖于lComponent属性和aComponent属性,如果lComponent属性和aComponent属性改变greenComponent属性也随之更改
+ (NSSet *)keyPathsForValuesAffectingGreenComponent
{
return [NSSet setWithObjects:@"lComponent", @"aComponent", nil];
}
- (double)greenComponent
{
return self.lComponent*self.aComponent*255.0;
}
//blueComponent属性依赖于lComponent属性和bComponent属性,如果lComponent属性和bComponent属性改变blueComponent属性也随之更改
+ (NSSet *)keyPathsForValuesAffectingBlueComponent
{
return [NSSet setWithObjects:@"lComponent", @"bComponent", nil];
}
- (double)blueComponent
{
return self.lComponent*self.bComponent*255.0;
}
//color属性依赖于redComponent属性和greenComponent属性和blueComponent属性,如果redComponent属性和greenComponent属性和blueComponent属性改变color属性也随之更改
+ (NSSet *)keyPathsForValuesAffectingColor
{
return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];
}

- (UIColor *)color
{
return [UIColor colorWithRed:self.redComponent/255.0 green:self.greenComponent/255.0 blue:self.blueComponent/255.0 alpha:1.];
}

观察变化

在ViewController这样写

添加观察者

1
2
3
4
5
6
7
8
self.labColor = [[LabColor alloc] init];
//添加监听
//第一个参数 observer:观察者 (这里观察self.labColor对象的属性变化)
//第二个参数 self 观察者
//第三个参数 keyPath: 被观察的属性名称(这里观察 self.myKVO 中 num 属性值的改变)
//第四个参数 options: 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
//第五个参数 context: 上下文,可以为 KVO 的回调方法传值(例如设定为一个放置数据的字典)
[self.labColor addObserver:self forKeyPath:@"color" options:NSKeyValueObservingOptionInitial context:&ColorKVOContext];

响应观察者

1
2
3
4
5
6
7
8
9
10
//响应观察
//keyPath:属性名称
//object:被观察的对象
//change:变化前后的值都存储在 change 字典中
//context:注册观察者时,context 传过来的值
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if (context==&ColorKVOContext) {
self.colorView.backgroundColor = self.labColor.color;
}
}

修改Model属性

1
2
3
4
5
6
7
8
9
- (IBAction)updateRedSlider:(UISlider *)sender {
self.labColor.lComponent = sender.value;
}
- (IBAction)updateGreenSlider:(UISlider *)sender {
self.labColor.aComponent = sender.value;
}
- (IBAction)updateBlueSlider:(UISlider *)sender {
self.labColor.bComponent = sender.value;
}

移除观察者

1
2
3
4
- (void)dealloc {
//在对象被释放时移除观察者,否则会出现BUG
[self.labColor removeObserver:self forKeyPath:@"color"];
}

Demo地址

原理

实现原理:

当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

深入剖析:

Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
(备注: isa 混写(isa-swizzling)isa:is a kind of ; swizzling:混合,搅合;)

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

①NSKVONotifying_A 类剖析:在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_A 类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。
(isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
—>我猜,这也是 KVO 回调机制,为什么都俗称KVO技术为黑魔法的原因之一吧:内部神秘、外观简洁。

②子类setter方法剖析:KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

1
2
3
4
-(void)setName:(NSString *)newName{ 
[self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用}