KVO 的实现

#是啥?

KVO 是通过一个 key 来找到某个属性并监听其值的改变。其实这也是一种典型的观察者模式。

#咋用?

  1. 添加观察者
  2. 在观察者中实现监听方法:-observeValueForKeyPath: ofObject: change: context:。
  3. 移除观察者

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Person.h
#import "Model.h"
@interface Person : NSObject <NSCoding>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSMutableArray *nickArr;
@property (nonatomic, strong) Model *model; // 包含了一个Model对象

@end

//Model.h
@interface Model : NSObject

@property (nonatomic, copy) NSString *name;

@end

在监听者类中注册监听的属性:

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
@interface AppDelegate()
@end

static void *nickArrContext = &nickArrContext;

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
Person *p = [[Person alloc] init];

//监听p.model.name字段的变化
[p addObserver:self forKeyPath:@"model.name"
options:NSKeyValueObservingOptionNew context:nil];
//赋值 触发观察者
p.model = [Model new];
p.model.name = @"211";

//监听数组属性的变化
[p addObserver:self forKeyPath:@"nickArr" options:NSKeyValueObservingOptionNew context:nickArrContext];
p.nickArr = [NSMutableArray arrayWithObject:@"david1"];//会触发回调
[p.nickArr addObject:@"Davi1"];//不会触发回调

NSMutableArray *mutNickArr = [p mutableArrayValueForKey:@"nickArr"];
//mutNickArr = [NSMutableArray arrayWithObject:@"david2"];如果重新赋值后再调用 addObject将不会触发KVO回调
[mutNickArr addObject:@"Davi2"];
NSLog(@"++++p.nickArr:%@",p.nickArr);

return YES;
}
#pragma mark -监听回调
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context{
id newValue = change[NSKeyValueChangeNewKey];

if ([keyPath isEqualToString:@"model.name"]) {
NSLog(@"++++model.name newValue:%@",newValue);
}else if ([keyPath isEqualToString:@"nickArr"]){
NSLog(@"++++nickArr newValue:%@",newValue);
}
}

输出日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
++++model.name newValue:<null>
++++model.name newValue:211
++++nickArr newValue:(
david1
)
++++nickArr newValue:(
Davi2
)
++++p.nickArr:(
david1,
Davi1,
Davi2
)

最后不要忘记在合适的时机(如dealloc或者回调中)移除监听。

1
[self removeObserver:self forKeyPath:@"name"];
  • 数组的监听

注意,在监听数组属性nickArr时,如果是给此数组重新赋值,会触发回调;但如果直接使用此数组添加对象addObject:时,不会触发回调!所以我们需要使用mutableArrayValueForKey:来返回一个原数组的代理数组对象,之后在此代理对象上的操作都会在原对象上有相同的效果,并且会收到回调。下面是此方法的使用说明:

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.

#KVO底层原理?

官方文档 解释如下:

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 交换的技术。当一个类的属性被观察时,Runtime 会动态的创建一个中间类,将原类的 isa 指针指向此中间类。中间类中重写原类被观察属性的 setter 方法,这样给原类属性赋值时调用的实际上是中间类的 setter 方法。重写的 setter 方法会调用 [super setValue:newName forKey:@”name”] 并在此方法前后分别插入 [self willChangeValueForKey:@“name”] 和 [self didChangeValueForKey:@”name”],以通知观察对象值的改变。

以上面案例中的 Model 为例,当 name 属性被观察后,aModel 对象的 isa 指针被指向了一个新建的 Model 的中间类 NSKVONotifying_Model,这个中间类重写了被观察值的 setter 方法和 class 方法、dealloc 及 _isKVO 方法,然后使 aModel 对象的 isa 指针指向这个新建的类。所以事实上 aModel 变为了 NSKVONotifying_Model 的实例对象,执行方法要从这个类的方法列表里找。

苹果警告我们,通过 isa 获取类的类型是不可靠的,通过 class 方法才能得到正确的类。

#主动触发KVO

如上所述,当属性发生变化时能收到通知,是因为 Runtime 使用 isa-swizzling 技术在中间类中属性的 setter 赋值语句前后主动调用了下面两个方法:

1
2
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

所以,当我们需要属性值的改变在符合某个条件时才触发 KVO 时,我们需要做以下几件事:

  • 重写类方法 +automaticallyNotifiesObserversForKey,禁用目标属性的系统 KVO 通知;
  • 定义属性并声明合成语句;
  • 提供属性的 getter、setter 函数并在 setter 中成员变量赋值前后加入上面两个方法;
  • 注册监听事件并重写回调函数;

#完整示例:

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
@interface Person : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@end

@implementation Person

@synthesize name = _name; //合成

//判断目标属性
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"name"]) {
return NO;//禁用系统的通知,不然会触发两次 KVO 回调
}
return YES;
}

//自己实现存取
- (NSString *)name{
return _name;
}

-(void)setName:(NSString *)name{

if ([name isEqualToString:@"David"]) {//设置条件
[self willChangeValueForKey:@"name"];// 主动触发通知
_name = name;
//其他业务逻辑
[self didChangeValueForKey:@"name"];
}else{
_name = name;
}
}
@end

调用并查看结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.person = [Person new];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
self.person.name = @"David";
return YES;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context{
NSLog(@"++++字段:%@, New:%@",keyPath,change);
[self.person removeObserver:self forKeyPath:@"name"];
}

KVO 的实现
https://davidlii.cn/2018/04/13/kvo.html
作者
Davidli
发布于
2018年4月13日
许可协议