一.概述:

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实现原理:

20180727174012225

KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。

在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。

并且将class方法重写,返回原类的Class。

所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

流程:

1、当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

2、派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

3、同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。