kvo是一种键值观察机制,能够通知对象,某个属性的值的发生了改变
注册为观察者
观察对象首先通过发送消息 addObserver:forKeyPath:options:context:将自身注册为观察者,这个必须得符合
kvc
的方式
添加观察者的方法
static void * viewContext = &viewContext;
// 这是创建一个静态变量,把自己的地址给变量
[self.view addObserver:self forKeyPath:NSStringFromSelector(@selector(backgroundColor)) options:NSKeyValueObservingOptionInitial context:viewContext];
选项的key
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
/* Whether the change dictionaries sent in notifications should contain NSKeyValueChangeNewKey and NSKeyValueChangeOldKey entries, respectively.
*/
//可以在通知中,检测到新值,NSKeyValueChangeNewKey,使用change[NSKeyValueChangeNewKey];
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
/* Whether a notification should be sent to the observer immediately, before the observer registration method even returns. The change dictionary in the notification will always contain an NSKeyValueChangeNewKey entry if NSKeyValueObservingOptionNew is also specified but will never contain an NSKeyValueChangeOldKey entry. (In an initial notification the current value of the observed property may be old, but it's new to the observer.) You can use this option instead of explicitly invoking, at the same time, code that is also invoked by the observer's -observeValueForKeyPath:ofObject:change:context: method. When this option is used with -addObserver:toObjectsAtIndexes:forKeyPath:options:context: a notification will be sent for each indexed object to which the observer is being added.
*/
//在监听的对象的属性改变之前,在添加观察者后,在方法返回之前,就会调用1次通知的方法
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
/* Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change. The change dictionary in a notification sent before a change always contains an NSKeyValueChangeNotificationIsPriorKey entry whose value is [NSNumber numberWithBool:YES], but never contains an NSKeyValueChangeNewKey entry. You can use this option when the observer's own KVO-compliance requires it to invoke one of the -willChange... methods for one of its own properties, and the value of that property depends on the value of the observed object's property. (In that situation it's too late to easily invoke -willChange... properly in response to receiving an -observeValueForKeyPath:ofObject:change:context: message after the change.)
When this option is specified, the change dictionary in a notification sent after a change contains the same entries that it would contain if this option were not specified, except for ordered unique to-many relationships represented by NSOrderedSets. For those, for NSKeyValueChangeInsertion and NSKeyValueChangeReplacement changes, the change dictionary for a will-change notification contains an NSKeyValueChangeIndexesKey (and NSKeyValueChangeOldKey in the case of Replacement where the NSKeyValueObservingOptionOld option was specified at registration time) which give the indexes (and objects) which *may* be changed by the operation. The second notification, after the change, contains entries reporting what did actually change. For NSKeyValueChangeRemoval changes, removals by index are precise.
*/
//这个key在对象中属性要发生改变的时候,就会调用一次,再发生改变后又会调用一次通知,两个方法可以根据`NSKeyValueChangeNotificationIsPriorKey`来进行区分,[change objectForKey:NSKeyValueChangeNotificationIsPriorKey]
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
收到监听的对象的属性的改变的通知
当一个被观察属性的值发生改变时,会观察者收到observeValueForKeyPath:ofObject:change:context:的消息。所有的观察者必须实现这个方法。这个方法中的参数和注册观察者方法的参数基本相同,一个只有change不同。change是一个字典,里面它所有游戏了的信息由注册时的options决定。
官方提供了这些关键我们给取来到change中的价值:
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
//观察kind的key,返回的是一个数字值,为NSKeyValueChange类型的值
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
//返回一个改变的新值
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
//返回原来的旧值
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
//这个键的价值是一个NSIndexSet,包含了发生插入,删除,替换的对象的索引集合
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
//这个关键包含了一个NSNumber,里面是一个布尔值,如果在注册时options中有NSKeyValueObservingOptionPrior,那么在前一个通知中的change中就会有这个关键的价值,我们可以这样来判断是不是在改变前的通知
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (context == viewContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(backgroundColor))]) {
// id objc = [change objectForKey:NSKeyValueChangeKindKey];
NSLog(@"%@ %@",change,[change objectForKey:NSKeyValueChangeNewKey]);
}
}
NSLog(@"%@ %@",keyPath,[change objectForKey:NSKeyValueChangeNewKey]);
}
当change[NSKeyValueChangeKindKey]是NSKeyValueChangeSetting的时候,说明被观察属性的setter方法被调用了。
而下面三种,根据官方文档的意思是,当被观察属性是集合类型,且对它进行了insert,remove,replace操作的时候会返回这三种关键,但是我自己测试的时候没有测试出来不知道是不是我理解错了。
-
NSKeyValueChangeNewKey
,NSKeyValueChangeOldKey
顾名思义,当你在注册的时候options参数中填了对应的NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
,并且NSKeyValueChangeKindKey
的值是NSKeyValueChangeSetting
,你就可以通过这两个键取到旧值和新值。 -
NSKeyValueChangeIndexesKey
,当NSKeyValueChangeKindKey
的结果是NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
的时候,这个键的价值是一个NSIndexSet
,包含了发生插入,删除,替换的对象的索引集合 -
NSKeyValueChangeNotificationIsPriorKey
,这个关键包含了一个NSNumber
,里面是一个布尔值,如果在注册时options中有NSKeyValueObservingOptionPrior
,那么在前一个通知中的change中就会有这个关键的价值,我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES;
上下文 context
可以使用NULL(就是不使用上下文的情况),判断只能根据
keyPath
唯一来来做校验,但是这样如果父类也同样监听了相同的属性,这样就会有问题
删除观察者对象
一般在dealloc中进行删除,一般使用@try @catch来实现,因为KVO不是正式协议,不能确定观察了哪个对象的属性,移除没有观察的属性,会奔溃,所有要使用@try @catch这个方法
-(void)dealloc{
NSLog(@"消失");
@try {
// 放可能发生错误代码,如果发生了异常,就会跳入@catch中,如果没有异常就不会进入@catch中
[self.view removeObserver:self forKeyPath:NSStringFromSelector(@selector(backgroundColor))];
} @catch (NSException *exception) {
//发生了异常,可以上报异常操作
} @finally {
// 不管有没有异常,都会执行这里
}
}
kvo合规性
有两种方式,能够使发送了改变,通知被发出
- 自动通知,继承自
NSObject
的,并且所有的属性符合KVC
规范,并实现KVO
的监听,不需要任何代码,就能自动实现 - 手动通知,可以在子类中实现
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
来返回NO使用手动通知
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:NSStringFromSelector(@selector(dataArray))]) {
return NO;
}else{
return [super automaticallyNotifiesObserversForKey:key];
}
return YES;
}
手动发送通知的时候的方法,在方法变化之前发送将要改变通知,在属性改变后发送已经改变完成的通知
-(void)addDataArrayObject:(NSString *)object{
[self willChangeValueForKey:NSStringFromSelector(@selector(dataArray))];
[self.dataArray addObject:object];
[self didChangeValueForKey:NSStringFromSelector(@selector(dataArray))];
}
多对多的关系
[self removeDataArrayAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]];
-(void)removeDataArrayAtIndexes:(NSIndexSet *)indexes{
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"dataArray"];
[self.dataArray removeObjectsAtIndexes:indexes];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"dataArray"];
}
键之间的依赖关系
一个属性的值可能由好几个相关的属性构成的,其中一个属性发生了变换,这个属性就会发生改变,也应该接受到改变通知
比如一个fullName是由firstName和lastName构成的,当如果firstName发生了改变,fullName应该接受到通知
@property (nonatomic,copy) NSString * fullName;
@property (nonatomic,copy) NSString * firstName;
@property (nonatomic,copy) NSString * lastName;
-(NSString *)fullName{
return [NSString stringWithFormat:@"%@%@",self.firstName,self.lastName];
}
- (void)viewDidLoad {
self.firstName = @"李";
self.lastName = @"明";
//监听fullName的值
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(fullName)) options:NSKeyValueObservingOptionNew context:nil];
//改变firstName的值
self.firstName = @"王";
}
//这个是增加依赖关系的,这个fullName是由firstName lastName构成的,实际上就是给fullName增加两个keypath属性,第一种设置依赖关系的方式
+(NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet * set = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:NSStringFromSelector(@selector(fullName))]) {
set = [set setByAddingObjectsFromArray:@[@"firstName",@"lastName"]];
}
return set;
}
//这个是增加依赖关系的,这个fullName是由firstName lastName构成的,第二种设置依赖关系的方式
+(NSSet *)keyPathsForValuesAffectingFullName{
return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}
多对多的关系
keyPathsForValuesAffectingValueForKey:
方法不能支持to-many的关系。举个例子,比如你有一个部门对象,和很多个雇员对象。而雇员有一个薪属属性。你可能希望部门对象有一个totalSalary的属性,依赖于所有的雇员的工资。
你可以注册部门成为所有雇员的观察者。当雇员被添加或者被移除时,你必须要添加和移除观察者。然后在observeValueForKeyPath:ofObject:change:context:
方法中,根据改变做出反馈。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}