欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

KVO用法总结

程序员文章站 2024-03-24 13:51:28
...

一.概述:KVO全称KeyValue

Observing,是苹果提供的一套事件通知机制。

作用:允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。

注意:由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO

使用KVO只需要两个步骤:

(1)注册Observer;

(2)接收通知。

  (3)   当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。建议在dealloc方法里移除

KVONSNotificationCenter都是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默认会自动通知观察者。

 

如果你想取消自动通知的方法是实现:

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key
方法,通过返回NO来控制取消自动通知。

针对非自动通知的属性,可以分别在变化之前和之后手动调用如下方法(will在前,did在后)来手动通知观察者:

  • (will/did)ChangeValueForKey:
  • (will/did)ChangeValueForKey:withSetMutation:usingObjects:
  • (will/did)Change:valuesAtIndexes:forKey:

手动通知的好处就是,可以灵活加上自己想要的判断条件.

事实上自动通知也是框架通过调用这些方法实现的。


被监听对象:

KVO用法总结KVO用法总结

监听对象:

学生类:

KVO用法总结 

 

mian函数:

KVO用法总结

 

结果:

KVO用法总结


1.监听对象必须实现接收方法。

2.先去判断是否自动通知还是手动。

3.当修改值的时候,会告诉监听对象。

 

 


 

KVO实现原理:

KVO用法总结

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

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

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

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

流程:

  1. 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
  2. 派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
  3. 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而**键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。