什么是KVO
KVO的全称是Key-Value Observing ,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者。KVO是在MVC应用程序中的各层之间进行通信的一种特别有用的技术。
KVO和NSNotification 都是 观察者模式的一种实现,
KVO和可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVC的mutableArrayValueForKey:等可变代理方法获取得到集合代理对象,并使用对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArray和NSSet。
KVO的基本使用
KVO使用三部曲:添加/注册 KVO 监听、实现监听方法以接收属性改变通知、移除 KVO监听。
1.调用方法 addObserver:forKeyPath:options:context:给被观察对象添加观察者;
2.在观察者类中实现 observerValueForKeypath:ofObject:change:context:方法以接收属性改变的通知消息;
3.当观察者不需要再监听时,调用removeObserver:forKeyPath:方法将观察者移除。需要注意的是,至少需要再观察者销毁之前,调用此方法,否则可能会导致Crash。
注册方法
/*
** target: 被观察对象
** observer:观察者对象
** keyPath: 被观察对象的属性的关键路径,不能为nil
** options: 观察的配置选项,包括观察的内容(枚举类型):
NSKeyValueObservingOptionNew:观察新值
NSKeyValueObservingOptionOld:观察旧值
NSKeyValueObservingOptionInitial:观察初始值,如果想在注册观察者后,立即接收一次回调,可以加入该枚举值
NSKeyValueObservingOptionPrior:分别在值改变前后触发方法(即一次修改有两次触发)
** context: 可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中的一种传值方式
如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash
*/
- (void)addObserver:(NSObject *)observer forKeypath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
监听方法
如果对象被注册成为观察者,则该对象必须能响应以下监听方法,即该对象所属类中必须实现监听方法。当被观察者对象属性发生改变时就会调用监听方法。如果没有实现就会导致Crash。
- (void)observeValueForKeyPath:(NSString *)keypath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
}
移除方法
在调用注册方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用以下方法移除观察者,否则如果在观察者被释放后,再次触发KVO监听方法就会导致Crash。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
使用例子
以下使用KVO为person对象添加观察者为当前viewController,监听 person 对象的 name 属性值的改变。当name值改变时,触发KVO的监听方法。
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = @"张三";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"keyPath:%@",keyPath);
NSLog(@"object:%@",object);
NSLog(@"change:%@",change);
NSLog(@"context:%@",context);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
实际应用
KVO主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO实现最为合适。斯坦福大学的ios教程中有一个很经典的案例,通过KVO在Model和Controller之间进行通信。如图:
KVO触发监听方法的方式
KVO触发分为自动触发和手动触发两种方式。
自动触发
如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发KVO:
使用点语法
使用setter方法
使用KVC的 setValue:forKey:方法
使用KVC的setValue:forKeyPath:方法
如果是监听集合对象的改变,需要通过KVC的mutableArrayValueForKey:等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO,集合对象包含NSAray和NSSet。
手动触发
普通对象属性火上成员变量使用:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
NSArray 对象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
NSSet 对象使用:
- (void)willChange:(NSKeyValueChange)changeKind valueAtIndexes forKey:(NSString *)key;
- (void)didChange:(NSKetValueChange)changeKind valuesAtIndexes forKey:(NSString *)key;
KVO的进阶使用
observationInfo 属性
observationInfo 属性是NSKeyValueObservering.h 文件中系统通过分类给NSObject 添加的属性,所以所有继承于NSObject 的对象都含有该属性;
可以通过 observationInfo 属性查看被观察对象的全部观察信息,包括observer\keyPath\options\context 等。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
context的使用
注册方法addObserver:forKeyPath:options:context: 中的context 可以传入任意数据,并且可以在监听方法中接收到这个数据。
context作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、多监听;也可以用来传值。
KVO 只有一个监听回调方法observeValueForKeyPath:ofObject:change:context: ,我们通常情况下可以在注册方法中指定context 为NULL ,并在监听方法中通过object 和keyPath 来判断触发KVO 的来源。但是如果存在继承的情况,比如现在有Person类和它的连个子类Teacher类和Student类,person、teacher和student实例对象都对account对象的balance属性进行观察。问题:
? 当balance发生改变时,应该由谁来处理呢?
? 如果都由person来处理,那么在Person类的监听方法中又该怎么判断是自己的事物还是子类对象的事物呢?
这个时候通过使用context就可以很好地解决这个问题,在注册方法中为context设置一个独一无二的值,然后在监听方法中对context值进行检验即可。
苹果的推荐用法:用context来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为context的值。可以为整个类设置一个context,然后在监听方法中通过object和keypath来确定被观察属性,这样存在继承的情况就可以通过context来判断;也可以为每个被观察对象属性设置不同的context,这样使用context就可以精确的确定被观察对象属性。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account *)account {
[account addObserver:self forKeyPath:@"balance" options:(NSKeyValueObservingOptionNEW | NSKeyValueObservingOptionOLd) context:PersonAccountInterestRateContext];
[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
}
else if(context == PersonAccountInterestRateContext) {
}
else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
context 优点:嵌套少、性能高、更安全、扩展性强。
Context 注意点:
? 如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就了能导致crash;
? 空传NULL而不应该传nil。
KVO监听集合对象
? KVO可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVC的mutableArrayValueForKey: 等方法获取得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArray 和 NSSet。
(注意:如果直接对集合对象进行操作改变,不会触发KVO)
示例及代码输出如下:
观察者viewController 对被观察对象person的myArray 属性进行监听。
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HRPerson new];
self.person.myArray = [NSMutableArray arrayWithCapacity:5];
[self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew | NSKeyValueObservevingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [self.person.mArray addObject:@"2"]; // 如果直接对数组进行操作,不会触发KVO
NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
[array addObject:@"1"];
[array replaceObjectAtIndex:0 withObject:@"2"];
[array removeObjectAtIndex:0];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
/* change 字典的值为:
{
indexes:对应的值为数组操作的详细信息,包括索引等
kind: 对应的值为数组操作的方式:
2:代表插入操作
3:代表删除操作
4:代表替换操作
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
new/old:如果是插入操作,则字典中只会有new字段,对应的值为插入的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew)
如果是删除操作,则字典中只会有old字段,对应的值为删除的元素,前提条件是options中传入了(NSKeyValueObservingOptionOld)
如果是替换操作,则字典中new和old字段都可以存在,对应的值为替换后的元素和替换前的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
*/
NSLog(@"%@",change);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"mArray"];
}
/*
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>number of indexes: 1 (in 1 ranges), indexes: (0)"; kind = 2; new = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>number of indexes: 1 (in 1 ranges), indexes: (0)"; kind = 4; new = (2); old = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>number of indexes: 1 (in 1 ranges), indexes: (0)"; kind = 3; old = (2); }
*/
KVO的自动触发控制
可以在被观察对象的类中重写
+ (BOOL)automaticalyNotifiesObserversForKey:(NSString *)key
方法类控制KVO的自动触发。
如果我们只允许外界观察oerson的name属性,可以在Person类如下操作。这样外界接只能观察name属性,即使外界注册了对person对象其他属性的监听,那么在属性发生改变时也不会触发KVO
// 返回值代表允不允许触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = YES;
}
else {
automic = [super automaticallyNotfiesObserversForKey:key];
}
return automatic;
}
也可以实现遵循命名规则为+(BOOL)autonaticallyNotifiesObserversOf 的方法来单一控制属性的KVO自动触发,为属性名(首字母大写)。
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO;
}
注意:
1.第一个方法的优先级高于第二个方法。如果实现了automaticallyNotifiesObserversForKey:方法,并对做了处理,则系统就不会再用该的automaticallyNotifiesObserversof方法。
2.options指定的NSKeyValueObservingOptionInitial 触发的KVO通知,是无法被automaticallyNotifiesObserversForKey:阻止的。
----------持续更新中
|