Key-value observing
是一种机制,允许对象在其他对象的指定属性发生更改时得到通知。它对于应用程序中 模型层 和 控制器层 之间的通信特别有用。
控制器对象 通常观察 模型对象 的属性,视图对象 通过 控制器 观察 模型对象 的属性。除此之外,模型对象 可以观察其他 模型对象(通常用于确定从属值何时更改),甚至可以观察自身(再次确定从属值何时更改)。
要使用KVO
,首先必须确保所观察的对象是符合KVO
的(实现NSKeyValueObServing
协议)。通常,如果你的对象继承自NSObject
,并且您以通常的方式创建属性,则您的对象及其属性将自动符合KVO
标准。也可以手动实现遵从性。KVO
遵从性描述了 自动 和 手动键值观察之间的区别,以及如何实现这两者。
只有在.h中暴露出来的属性,才能被KVO监听到,.m中的属性,是不能够被监听到。
KVO的基本使用
定义被观察的对象:
1 2 3 4 5 6 7
| @interface MyModel : NSObject
@property (nonatomic, assign)CGFloat amount; @property (nonatomic, strong)NSMutableArray *arr; @property (nonatomic, strong)NSMutableDictionary *dict;
@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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| static void *VCMyPersonAmountContext = &VCMyPersonAmountContext;
- (void)viewDidLoad { [super viewDidLoad]; self.model = [[MyModel alloc] init]; self.model.amount = 100; self.model.amount = 200;
[self.model addObserver:self forKeyPath:@"amount" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:VCMyPersonAmountContext]; }
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.model.amount = 102.23; }
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context { if (context == VCMyPersonAmountContext) { NSLog(@"change = %@", change);
} else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
- (void)dealloc{ [self.model removeObserver:self forKeyPath:@"amount" context:VCMyPersonAmountContext]; }
|
KVO的合规性
为了让类特定属性是符合KVO
,类必须确保以下各项:
- 该类必须使得该属性支持
KVC
。
- 该类为更改属性发出
KVO
通知。
- 当有依赖关系的时候,注册合适的依赖键。
更改通知
有两种技术可以确保发出更改通知:
- 自动更改通知,由
NSObject
提供,默认情况下,它适用于符合KVC
类的所有属性。通常,如果遵循标准的Cocoa
编码和命名约定,就可以使用自动更改通知,而不必编写任何其他代码。
- 手动更改通知,它提供了对何时发出通知的额外控制,需要额外的编码,通过实现类方法
automaticallyNotifiesObserversForKey:
可以控制子类属性的自动通知。
自动更改通知
NSObject
提供了自动键值更改通知的基本实现。自动键值更改通知 通知观察者 使用 符合Key-Value
访问器 以及 KVC
方法所做的更改。
通过如下方法更改被观察者的属性的值,都会发送通知:
1 2 3 4 5 6
| [account setName:@"Savings"]; [account setValue:@"Savings" forKey:@"name"];
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"]; [transactions addObject:newTransaction];
|
手动更改通知
通过手动通知你可以控制特定属性通知的过程,例如:尽量减少因特定原因而不必要触发通知,或者将多个更改分组为单个通知。
手动通知和自动通知不是互斥的,可以同时存在,在这种情况下,通过重写NSObject
的automaticallyNotifiesObserversForKey:
类方法来实现。
如下代码实现手动更改通知:
1 2 3 4 5 6 7
| @interface MyBus : NSObject
@property (nonatomic, copy)NSString *name; @property (nonatomic, assign)NSInteger numberID; @property (nonatomic, strong)NSArray *arr;
@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
| @implementation MyBus
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { BOOL automatic = NO; if ([key isEqualToString:@"arr"]) { automatic = NO; } else if ([key isEqualToString:@"numberID"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:key]; } return automatic; }
- (void)setArr:(NSMutableArray *)arr { if (![arr isEqual:@[]]) { [self willChangeValueForKey:@"arr"]; _arr = arr; [self didChangeValueForKey:@"arr"]; } }
- (void)setNumberID:(NSInteger)numberID { [self willChangeValueForKey:@"numberID"]; [self willChangeValueForKey:@"name"]; _numberID = numberID; _name = [NSString stringWithFormat:@"%@_%ld", _name, numberID]; [self didChangeValueForKey:@"name"]; [self didChangeValueForKey:@"numberID"]; }
@end
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| self.bus = [[MyBus alloc] init]; self.bus.name = @"新能源公交车"; [self.bus addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; [self.bus addObserver:self forKeyPath:@"arr" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; [self.bus addObserver:self forKeyPath:@"numberID" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
self.bus.arr = @[]; self.bus.numberID = 100;
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context { NSLog(@"%@, %@", keyPath, change); }
|
注册依赖键
在许多情况下,一个属性的值取决于另一对象中一个或多个其他属性的值。如果一个属性的值发生更改,则派生属性的值也应标记为更改。如何确保为这些依赖属性发布KVO
通知取决于关系的基数。
一对一关系
为了让一对一关系自动触发通知,需要重写 keyPathsForValuesAffectingValueForKey:
或者 实现一个合适的方法,该方法遵循它为 注册依赖键 定义的模式。
1 2 3 4 5 6
| @interface MyCar : NSObject
@property (nonatomic, copy)NSString *name; @property (nonatomic, assign)CGFloat price;
@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
| @interface MyToy : NSObject
@property (nonatomic, strong)MyCar *car; @property (nonatomic, copy)NSString *info;
@end
- (NSString *)info { return [NSString stringWithFormat:@"我的玩具车名为:%@,花了我%f元钱", self.car.name, self.car.price]; }
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *set = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"info"]) { NSArray *affectingKeys = @[@"car.name", @"car.price"]; set = [set setByAddingObjectsFromArray:affectingKeys]; } return set; }
@end
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| self.toy = [[MyToy alloc] init]; MyCar *car = [[MyCar alloc] init]; car.name = @"赛车"; car.price = 50.5; self.toy.car = car;
[self.toy addObserver:self forKeyPath:@"info" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
self.toy.car.name = @"超人赛车";
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context { NSLog(@"%@, %@", keyPath, change); }
|
一对多关系
keyPathsForValuesAffectingValueForKey:
不支持包含一对多关系的key-Path。例如如下例子:一个Department
的对象包含 多个Employee
的对象,且Department
对象的totalSalarys
属性依赖多个Employee
对象的salary
属性的总和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @interface Employee : NSObject
@property (nonatomic, strong)NSNumber *salary;
- (instancetype)initWithSalary:(NSNumber *)salary;
@end
@interface Department : NSObject
@property (nonatomic, strong)NSArray <Employee *>*employees; @property (nonatomic, strong)NSNumber *totalSalarys;
@end
|
不能使用 keyPathsForValuesAffecting<Key>
方法返回 @"employee.salary"
去实现其依赖关系,从而自动发出通知。
当出现 一对多的依赖关系时,有一下两种方法解决:
方法一
将 父级 注册为所有 子级 的观察者。具体实现如下:
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
| static void *totalSalaryContext = &totalSalaryContext;
@implementation Department
- (instancetype)init { if (self = [super init]) { [self addObserver:self forKeyPath:@"employees" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:totalSalaryContext]; } return self; }
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if (context == totalSalaryContext) { [self updateTotalSalary]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
- (void)updateTotalSalary { NSNumber *num = [self valueForKeyPath:@"employees.@sum.salary"]; [self setTotalSalary:num]; } - (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (_totalSalarys != newTotalSalary) { [self willChangeValueForKey:@"totalSalarys"]; _totalSalarys = newTotalSalary; [self didChangeValueForKey:@"totalSalarys"]; } }
- (NSNumber *)totalSalary { return _totalSalarys; }
- (void)dealloc { [self removeObserver:self forKeyPath:@"totalSalary" context:totalSalaryContext]; }
@end
|
监听一对多依赖关系中属性的更改通知:
1 2 3 4 5 6 7 8 9 10 11
| self.dept = [[Department alloc] init]; [self.dept addObserver:self forKeyPath:@"totalSalarys" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
self.dept.employees = @[[[Employee alloc] initWithSalary:@10], [[Employee alloc] initWithSalary:@20]];
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context { NSLog(@"👍👍👍 %@, %@", keyPath, change); }
|
方法二
如果您正在使用Core Data
,可以向应用程序的notification center
注册父对象,作为其托管对象上下文的观察者。父级应以类似于KVO
的方式响应子级发布的相关更改通知。
KVO 监听 NSAarry 和 NSSet 集合对象的某个元素
1 2 3 4 5 6 7 8 9 10 11
| self.dept.employees = @[[[Employee alloc] initWithSalary:@10], [[Employee alloc] initWithSalary:@20]];
[self.dept.employees addObserver:self toObjectsAtIndexes:[NSIndexSet indexSetWithIndex:0] forKeyPath:@"salary" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
self.dept.employees.firstObject.salary = @1000;
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context { NSLog(@"👍👍👍 %@, %@", keyPath, change); }
|
KVO的原理
Automatic key-value observing
是使用一种称为isa-swizzling
的技术实现的。
isa
指针,指向对象的类,该类维护一个调度表。这个调度表实际上包含 指向该类实现的方法的指针 和 其他数据。
当为 对象的属性 注册 观察者 时,将修改 被观察对象 的 isa
指针,指向 中间类 而不是 真正类。因此,isa
指针的值不一定反映实例的实际类。
决不能依赖isa
指针来确定类成员身份。应该使用class
方法来确定对象实例的类。
当添加KVO
监听一个A类
的属性时,系统会在runtime
动态地创建该类的一个子类NSKVONotifyig_A
,并且在NSKVONotifyig_A
中重写基类中任何被观察属性的 setter
方法。
当我们修改A类
的一个被观察的属性时,系统其实将A类
的isa
指针指向了子类NSKVONotifyig_A
,走NSKVONotifyig_A
的setter
方法,setter
方法随后负责 通知观察对象属性的改变状况 以及 调用A所属类的setter
方法,从而激活键值通知机制。
1 2 3 4 5
| - (void)setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; [super setValue:newName forKey:@"name"]; [self didChangeValueForKey:@"name"]; }
|
学习博客
Key-Value Observing Programming Guide
Key-Value Coding Programming Guide
iOS大解密:玄之又玄的KVO
sunnyxx * objc kvo简单探索
Runtime窥探 (五)| KVO底层实现
如何优雅地使用 KVO
iOS 底层探索 - KVO