KVO: 全称--- Key-value observing
探索KVO原理和KVO一样,通过官方文档去看。
KVO使用流程
简单三步骤:
1、注册观察者
2、实现方法来获取观察者属性改变的通知
3、移除观察者
- 官方文档的介绍
You must perform the following steps to enable an object to receive key-value observing notifications for a KVO-compliant property:
必须执行以下步骤,才能使对象接收KVO兼容属性通知的键值:
* Register the observer with the observed object using the method addObserver:forKeyPath:options:context:
将观察者注册到观察对象上 使用这个方法:addObserver:forKeyPath:options:context:
* Implement observeValueForKeyPath:ofObject:change:context: inside the observer to accept change notification messages.
实现 observeValueForKeyPath:ofObject:change:context: 来接收观察者内部值的变化的通知消息。
* Unregister the observer using the method removeObserver:forKeyPath: when it no longer should receive messages. At a minimum, invoke this method before the observer is released from memory.
在观察者从内存释放之前,调用removeObserver:forKeyPath:来移除观察者
- 官方示例
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
参数的意义
1、 context
context 在官方文档有介绍:
The context pointer in the `addObserver:forKeyPath:options:context:` message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify `NULL` and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the `balance` and `interestRate` properties chosen this way.
**Listing 1** Creating context pointers
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
content 相对于keyPath,更安全、便利、直接,更好扩展的方式来区分接收到的消息来源于哪个对象
context 包含将在相应的更改通知中传递回观察者的任意数据。您可以指定NULL并完全依赖keyPath字符串来确定通知的来源,但是这种方式可能会导致一个问题,父类的一个对象由于不同的原因也在观察同一个路径。
nil、Nil、NULL的区别
2、 Options
-
NSKeyValueObservingOptionNew
: 变化之后的值 -
NSKeyValueObservingOptionOld
:变化之前的值 -
NSKeyValueObservingOptionInitial
: 作用是 注册观察者的时候,立即向观察者发送通知(添加观察,就会调用一次回调)。
如果同时有OptionOld的情况下,第一次回调是没有old value的 -
NSKeyValueObservingOptionPrior
:在value发生变化之前发送一次通知,value变化之后正常发送一次通知。(也就是willChange 和 didChange都发送通知)。
在变化前的通知(第一次通知)中,会包含NSKeyValueChangeNotificationIsPriorKey ,不包含NewKey(即使观察OptionNew,在第一次通知中,newKey也会被丢弃)
3、 NSKeyValueChangeKey
-
NSKeyValueChangeKindKey
触发KVO的来源typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, //调用set方法// 观测的值是一个可变集合/数组的情况
NSKeyValueChangeInsertion = 2, //插入操作
NSKeyValueChangeRemoval = 3, //移除操作
NSKeyValueChangeReplacement = 4, // 替换操作
}; NSKeyValueChangeNewKey
: 新值的key
⚠️:当KindKey 是2/3/4的时候,newKey是变化的那个元素,而不是变化后的数组。具体查看 注意点-4NSKeyValueChangeIndexesKey
: 对数组操作时(NSKeyValueChange 不是 1 的情况下)有效indexes="<_NSCachedIndexSet: 0x7fde0a50cd20>[number of indexes: 1 (in 1 ranges), indexes: (0)]"
NSKeyValueChangeNotificationIsPriorKey
: 观察Options为OptionPrior的情况下,发送第一次(willChange)通知的时候有效
其他知识点
1、自动调用/手动调用
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
是否开启自动调用。
- 自动调用:系统在合适的地方会自动添加 willChange和didChange,在这种情况下触发的,称之为自动调用
- 手动调用:有时候在重写set方法的时候,会添加 willChange和didChange方法(最底下 KVO注意点-第三点),自己添加change方法的情况下触发的KVO称之为手动调用。
还记得在KVC原理里面,有提到一个 是否自动去匹配相关的成员变量的方法:accessInstanceVariablesDirectly
有些类似,都是一个开关方法。
2、多因素影响(比如下载进度)
下载进度的多少 取决于 总下载数据量和已下载数据量
@interface Person : NSObject
@property (nonatomic, assign) double getData; //以获取data
@property (nonatomic, assign) double totalData; //总data
@property (nonatomic, assign) double progress; //进度
@end
@implementation Person
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"progress"]) {
NSArray *affectingKeys = @[@"totalData", @"getData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
@end
//ViewController.m
_person = [Person new];
_person.totalData = 100;
[self addKVOCHild];
- (void)addKVOCHild {
[_person addObserver:self forKeyPath:@"progress"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:viewkeyPaht];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == viewkeyPaht) {
NSLog(@"progress = %f",_person.getData/_person.totalData);
}else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
_person.getData += 20;
_person.totalData += 15;
}
//每次touch,会回调2次 因为getData、totalData 都会影响progress
3、数组KVO触发方式
对数组操作的普通写法 添加、删除、替换:[_person.dateArray addObject:@"1"];
无法触发KVO。
如果要在add、replace、remove的时候,可以触发KVO,需要借助KVC的方式
[[_person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
注意触发KVO的change:kind 不再是1了
查看 ---< 注意点-4 > -----
KVO注意点
1、通常在接收通知消息中,对观察对象进行处理,没有处理的应该通过
super observeValueForKeyPath ....
的方式传递到上一层。
这里就存在一个问题:如果父类、父类的父类...都没有处理,就会抛出NSInternalInconsistencyException
异常:message was received but not handled
,所以,你自己添加的观察对象,你需要自己处理2、注册观察者的时候如果context不是NULL,那么在移除观察的时候,应该使用
removeObserver: forKeyPath: context:
方法去移除。
详细原因可查看addObserver:forKeyPath:options: context:
的注释。简单的说,就是:当同一个观察者多次注册到同一个keyPath,但每次使用context时,-removeObserver:forKeyPath:在决定要删除的内容时,他会猜测应该移除哪一个context,它可能猜错了。3、对于添加的属性,如果需要对其进行观察,在set方法中
不应该
手动加入willchange
和didChange
方法,因为这样会导致接收消息触发2次,
系统在生成set方法的时候,会在合适的时候自动添加ChangeValue方法,即使重写了set方法,依然不需要手动添加
在分类添加属性并重写set方法的时候,也是不需要的。
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"]; //这一行是不需要的
_name = name;
[self didChangeValueForKey:@"name"]; //这一行是不需要的
}
那么ChangeValue是在什么情况下用的呢? 是在不调用set方法的情况下,想手动触发KVO消息。
- 4、对于数组的KVO,使用
[[_testModel mutableArrayValueForKey:@"childArr"] removeObject:@"1"];
这个方法才能触发KVO,但是还有一点需要注意,KVO观察到的NSKeyValueChangeNewKey
的值,并不是 childArr的全部内容,而是变化的那个元素。比如一下代码:
static void *viewkeyPaht = &viewkeyPaht;
{
_testModel.childArr = @[@"1",@"2",@"3"].mutableCopy;
[_testModel addObserver:self forKeyPath:@"childArr" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[[_testModel mutableArrayValueForKey:@"childArr"] addObject:@"1"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == viewkeyPaht) {
NSLog(@"change = %@",change);
}else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// 在touchesBegan 方法里进行remove、add、replace操作 分别打印结果
// addObject
change = {
indexes = "<_NSCachedIndexSet: 0x600003df0900>[number of indexes: 1 (in 1 ranges), indexes: (3)]";
kind = 2;
new = (
4
);
}
// remove
change = {
indexes = "<_NSCachedIndexSet: 0x600001b772a0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 3;
old = (
2
);
}
// replace
change = {
indexes = "<_NSCachedIndexSet: 0x6000032e93a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 4;
new = (
4
);
old = (
1
);
}
从上面的打印结果看,对数组操作,new和old都是针对变化的那个元素,而不是变化前后数组的值
还有一点注意到:
addObject 操作:OldKey被抛弃
remove 操作:NewKey被抛弃