KVO的简单实现

2017/9/1 posted in  iOS

之间看有关OCiOS的书都会看到KVO这个名词。所以今天来学习和实现一下。简单的说KVOKey-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。

它来源于设计模式中的观察者模式,其基本思想就是:

一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

我们一般用到的都是自动实现KVO,所以我这里就不实现手动的KVO了。实现KVO需要两个主要的方法:

//注册观察者
- (void)addObserver:(NSObject *)observer 
        forKeyPath:(NSString *)keyPath 
        options:(NSKeyValueObservingOptions)options 
        context:(void *)context;
        
//当观察的对象属性有所改变就会通知观察者,该方法用来处理变更通知
- (void)observeValueForKeyPath:(NSString *)keyPath 
                     ofObject:(id)object 
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
                        context:(void *)context;

这两个方法在Foundation/NSKeyValueObserving.h中,NSObject,NSArray,NSSet均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或结合类对象。

我们来举例说明:

创建一个观察者类:

//
//  Observer.h
//  KVO机制
//

#import <Foundation/Foundation.h>

@interface Observer : NSObject

@end



//  Observer.m
//  KVO机制
#import "Observer.h"

@implementation Observer

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    //判断发送过来的通知中更改的属性是否是name
    if ([keyPath isEqualToString:@"name"]) {
        //获取更改属性的类的信息
        Class classInfo = (__bridge Class)context;
        NSString *className = [NSString stringWithFormat:@"%s", object_getClassName(classInfo)];
        NSLog(@" >> class : [%@] , Name changed", className);
        NSLog(@" >> old name is %@", [change objectForKey:@"old"]);
        NSLog(@" >> new name is %@", [change objectForKey:@"new"]);
    
    }else{
        /*
         *注意:在实现处理变更通知方法 observeValueForKeyPath 时,
         *要将不能处理的 key 转发给 super 的 observeValueForKeyPath 来处理。
         */
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

然后创建一个用于观察的Target

//  Target.h
//  KVO机制


#import <Foundation/Foundation.h>

@interface Target : NSObject

@property(nonatomic,assign) NSString *name;

@end


//  Target.m
//  KVO机制


#import "Target.h"

@implementation Target
//给name属性一个初始值,用于检测变化
-(instancetype)init{
    if (self = [super init]) {
        _name = @"yue";
    }
    return self;
}
@end

然后我们在main.m中添加观察者来执行:

//  main.m
//  KVO机制


#import <Foundation/Foundation.h>
#import "Observer.h"
#import "Target.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //分别创建实例
        Observer *observer = [[Observer alloc]init];
        Target *target = [[Target alloc]init];
        
        //target 增加一个观察者,用于观察name属性
        [target addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)([Target class])];
        //更改name属性的值,响应观察者的动作
        [target setName:@"hao"];
        //移除name的观察者,防止内存泄露
        [target removeObserver:observer forKeyPath:@"name"];
    }
    return 0;
}

我们运行后结果如下:

可以发现是Target类发送过来的通知,说明属性已经变更。

下面我们来讨论一下传过来了的参数。其中addObserver方法中的option:参数用于指定应该包含哪种通知。其中主要有以下几种:

  • NSKeyValueObservingOptionNew:指出change字典应该包含有新的属性(如果适用)。
  • NSKeyValueObservingOptionOld:指出change字典应该包含有旧的属性(如果适用)。
  • NSKeyValueObservingOptionInitial:把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
  • NSKeyValueObservingOptionPrior: 分2次调用。在值改变之前和值改变之后。

其中observeValueForKeyPath方法中的change参数是一个字典型数据。会根据option参数的变化来生成不同的数据。一般会包含newold两个key