KVO用法总结
一.概述:
KVO全称Key-Value Observing,是苹果提供的一套事件通知机制。
作用:允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。
注意:由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。 使用KVO的步骤:
(1)注册Observer;
(2)接收通知。
(3)当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。建议在dealloc方法里移除
KVO和NSNotification都是iOS中观察者模式的一种实现。
区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
二.KVO注册与接收通知:
1.注册Observer:
使用方法: addObserver:forKeyPath:options:context:
参数含义:
1. observer:观察者,监听属性变化的对象。该对象必须实现 observeValueForKeyPath:ofObject:change:context: 方法。
2. keyPath:要观察的属性名称。要和属性声明的名称一致。
3. options:对KVO机制进行配置,修改KVO通知的时机以及通知的内容
4. context: 传入任意类型的对象,在"接收消息回调"的代码中可以接收到这个对象,是KVO中的一种传值方式。
options参数:
enum {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial = 0x04,
NSKeyValueObservingOptionPrior = 0x08
};
typedef NSUInteger NSKeyValueObservingOptions;
默认只接受新值
NSKeyValueObservingOptionNew:接收方法中使用change参数传入变化后的新值,键为:NSKeyValueChangeNewKey;
NSKeyValueObservingOptionOld:接收方法中使用change参数传入变化前的旧值,键为:NSKeyValueChangeOldKey;
NSKeyValueObservingOptionInitial:注册之后立刻调用接收方法,如果配置了NSKeyValueObservingOptionNew,change参数内容会包含新值,键为:NSKeyValueChangeNewKey;
NSKeyValueObservingOptionPrior:如果加入这个参数,接收方法会在变化前后分别调用一次,共两次,变化前的通知change参数包含notificationIsPrior = 1。
注册Observer之后一定要在合适的机会解除注册,否则会引发资源泄露,取消注册的方法:
removeObserver:forKeyPath:context
一般在dealloc方法里删除
2.接收通知:
注册后,当属性的值发生变化时,框架默认会自动通知注册的观察者。
当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
object:这个是所监听的对象,也就是所监听的属性所属的对象。
change:是传入的变化量,通过在注册时用options参数进行的配置,会包含不同的内容。
其他参数含义同注册时方法的参数含义。
在实现这个方法中需要注意的是, 一定要对注册监听的所有属性都进行处理——使用context参数进行判断——否则Xcode会警告。
change参数:
除了根据options参数控制的change参数内容,默认change参数会包含一个NSKeyValueChangeKindKey键值对,传递被监听属性的变化类型:
enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
NSKeyValueChangeSetting:属性的值被重新设置;
NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement:表示更改的是集合属性,分别代表插入、删除、替换操作。
如果被观察对象是集合对象 在NSKeyValueChangeKindKey包含NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement的信息,表示集合对象的操作方式,change参数还会包含一个NSKeyValueChangeIndexesKey键值对,表示变化的index。
3.例子:
#import <Foundation/Foundation.h>
@interface Book : NSObject
@property (nonatomic,strong)NSString *name;
@property (nonatomic,strong)NSString *price;
@end
#import "ViewController.h"
#import "Book.h"
@interface ViewController ()
@property (nonatomic,strong)Book *abook;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self addObserver];
[self addBtn];
}
/**
添加监听
*/
-(void)addObserver{
//添加监听
self.abook = [[Book alloc]init];
self.abook.price = @"0";//先设一个初始值
[_abook addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
}
/**
添加一个按钮
*/
-(void)addBtn{
UIButton *abtn = [UIButton buttonWithType:UIButtonTypeCustom];
abtn.frame = CGRectMake(80, 90.0, 80, 30);
[abtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[abtn setTitle:@"Change" forState:UIControlStateNormal];
[abtn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:abtn];
}
/**
按钮点击事件
*/
-(void)btnClick{
NSLog(@"点击了Btn!");
NSInteger randomPrice = arc4random() % 100;
NSString *newPrice = [NSString stringWithFormat:@"%ld",(long)randomPrice];
//触发监听
//第一种方法
// NSDictionary *newBookPropertiesDictionary=[NSDictionary dictionaryWithObjectsAndKeys:
// @"book name",@"name",
// newPrice,@"price",nil];
// [self.abook setValuesForKeysWithDictionary:newBookPropertiesDictionary];
//第二种方法
[self.abook setValue:newPrice forKey:@"price"];
不仅可以通过点语法和set语法进行调用,KVO兼容很多种调用方式。
}
//实现监听
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqual:@"price"]) {
NSLog(@"old price: %@",[change objectForKey:@"old"]);
NSLog(@"new price: %@",[change objectForKey:@"new"]);
}
}
-(void)dealloc
{
//移除监听
[_abook removeObserver:self forKeyPath:@"price"];
}
@end
打印结果:
KVOTest[40935:3327447] 点击了Btn!
KVOTest[40935:3327447] old price: 0
KVOTest[40935:3327447] new price: 87
KVOTest[40935:3327447] 点击了Btn!
KVOTest[40935:3327447] old price: 87
KVOTest[40935:3327447] new price: 49
触发监听方法:
1.直接调用set方法,或者通过属性的点语法间接调用
2.使用KVC的setValue:forKey:方法
3.使用KVC的setValue:forKeyPath:方法
4.通过mutableArrayValueForKey:方法获取到代理对象,并使用代理对象进行操作
三、KVO实现原理:
KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。
在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。
并且将class方法重写,返回原类的Class。
所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
流程:
1、当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
2、派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
3、同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。