首页 > 代码库 > Key-Value Observing (键值监测)

Key-Value Observing (键值监测)

Key-Value Observing (键值监测)

简介

KVO是一套当目标对象的属性值改变时观察者对象能够接受到通知的机制。必须先理解KVC才能更好的理解KVO,前者是后者的实现基础。
这样的通信机制在MVC设计模式很是常见
技术分享
实现过程简单来说分为3步:
1、添加观察这和监测对象
2、监测对象改变
3、收到值改变通知,处理后续逻辑
举个生活中的例子就是给银行卡开通短信通知的业务,总体也是分3步“
1、去银行办理短信业务
2、账号财产变动
3、收到短信通知
KVO是框架级别的服务,无需自己发送通知,使用方便,基本不需要添加额外代码即可使用。

详情

为了使用KVO,必须满足以下3步

1、目标对象的属性,必须支持KVO

2、注册观察者与被观察者addObserver:forKeyPath:options:context:

3、观察者必须实现observeValueForKeyPath:ofObject:change:context:方法

第一步、确保目标支持KVO

被监测的目标对象的属性支持KVO必须满足以下条件:

1、目标对象的属性必须支持KVC,对于1对1属性简单来说就是实现set和get方法。详情和1对多请阅读官方说明。系统已有类及子类自动支持,放心使用。

2、自动和手动属性通知
目标对象必须能发出属性变化通知。系统默认支持,也可自定义。
系统默认支持,且支持的很好,一般无需自定义。

//如果需要自定义,需要重新此方法,默认返回YES
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

//在set方法中手动调用,变化类型只是针对NSKeyValueChangeSetting
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

例如

//假设有属性
@property (nonatomic,copy)NSString *name;


+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {

    BOOL automatic = NO;
    /* 只自定义指定属性,其它仍然自动发送通知 */
    if ([theKey isEqualToString:@"name"])
    {
        //在set方法中手动调用相关方法
        automatic = NO;
    }
    else
    {
        //此方法默认返回YES
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

- (void)setName:(NSString *)name
{
    //即将变化
    [self willChangeValueForKey:@"name"];

    _name = name;

    //已经变化
    [self didChangeValueForKey:@"name"];
}

//如果说只有值不相等时才发送通知,提升性能
- (void)setName:(NSString *)name
{
    if (![name isEqualToString:_name])
    {

        [self willChangeValueForKey:@"name"];

        _name = name;

        [self didChangeValueForKey:@"name"];
    }
}

如果涉及1对多的容器类,需要自己实现 NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement三种操作对应的方法,例如

//Keys为属性名称
- (void)removeKeysAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"keys"];

    // Remove the transaction objects at the specified indexes.

    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"keys"];
}

3、属性依赖
如果目标对象属性存在依赖关系,注册合适的依赖Keys。核心方法为

第一种、
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key NS_AVAILABLE(10_5, 2_0);
说明:
1、返回目标属性依赖属性的KeyPath的Set。当对象注册后,KVO自动监测该对象所有的KeyPaths。
2、其默认实现从对象所属类的方法列表中匹配方法:+keyPathsForValuesAffecting<Key>(<Key>为属性名,比如Name),如果存在执行并返回结果;如果不存在向底层寻找已经废弃的方法+setKeys:triggerChangeNotificationsForDependentKey:
3、可以用来替换手动调用-willChangeValueForKey:/-didChangeValueForKey:来实现属性依赖的解决方案
4、不能在已有类的Category中使用,在Category禁止重写此方法,可以使用+keyPathsForValuesAffecting<Key>来实现。

第二种、
或者重写此格式+keyPathsForValuesAffecting<Key>(<Key>为属性名,比如Name)的方法名

比如说,姓名=姓+名;当二者任一变动时更新姓名

@property (nonatomic,copy)NSString *name;//姓名
@property (nonatomic,copy)NSString *firstName;//姓
@property (nonatomic,copy)NSString *lastName;//名

//重新get方法,表明字段组成
-(NSString *)name
{
    return [NSString stringWithFormat:@"%@%@",_firstName,_lastName];
}

//通过此方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key 
 {

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@"name"])
    {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
或者
+ (NSSet *)keyPathsForValuesAffectingName
{
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

//改变值
- (void)viewDidLoad {
    [super viewDidLoad];

    [self setValue:@"张" forKey:@"firstName"];
    [self setValue:@"三" forKey:@"lastName"];

    [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

    //名称改变,刷新姓名
    [self setValue:@"龙" forKey:@"lastName"];


}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
    NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
}

//输出结果
2016-09-06 17:05:01.904 KVC[4192:307124] NSKeyValueChangeOldKey:张三
2016-09-06 17:05:01.904 KVC[4192:307124] NSKeyValueChangeNewKey:张龙

注意:以上关于属性依赖的处理方法不支持一对多的关系。比如说ViewController对象有一个totalNumber表示总数和属性datas数组,数组中Data的对象,对象含有number属性。

/* Data对象 */
@interface Data : NSObject

@property (nonatomic,assign)NSInteger number;

@end


/* ViewController对象 */
@interface ViewController ()

@property (nonatomic,assign)NSInteger totalNumber;

@property (nonatomic,strong)NSArray *datas;

@end

可以采用以下方法解决
1、注册每个Data对象的年龄属性为监测属性,ViewController对象为观察者,data.number变化时,使用集合运算符求和,然后设置ViewController的totalNumber属性值

- (void)viewDidLoad {
    [super viewDidLoad];

    /* 创建Data对象 */
    Data * data1 = [[Data alloc] init];
    data1.number = 0;

    Data *data2 = [[Data alloc] init];
    data2.number = 0;

    Data *data3 = [[Data alloc] init];
    data3.number = 0;

    /* self.datas属性赋值 */
    [self setValue:@[data1,data2,data3] forKey:@"datas"];

    /* 监测self.datas中每个data对象的number属性 */
    //(0, 3) 中0表示index从0开始,0表示长度3,也就是index(0、1、2);但是不能使得self.datas数组越界
    NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 3)];

    [self.datas addObserver:self toObjectsAtIndexes:set forKeyPath:@"number" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

    /* 监测totalNumber属性 */
    [self addObserver:self forKeyPath:@"totalNumber" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

    /* 改变值,查看输出结果 */
    [data1 setValue:@"1" forKey:@"number"];
    [data2 setValue:@"1" forKey:@"number"];
    [data3 setValue:@"1" forKey:@"number"];

}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"number"])
    {
        [self updateTotalNumber];
    }
    else if ([keyPath isEqualToString:@"totalNumber"])
    {
        NSLog(@"%@--%@",keyPath,change);
    }
}

//更新总数
- (void)updateTotalNumber
{

    NSNumber *total = [self valueForKeyPath:@"datas.@sum.number"];

    [self setValue:total forKey:@"totalNumber"];
}

//输出结果
2016-09-07 14:10:10.694 KVC[3034:165515] totalNumber--{
    kind = 1;
    new = 1;
    old = 0;
}
2016-09-07 14:10:10.695 KVC[3034:165515] totalNumber--{
    kind = 1;
    new = 2;
    old = 1;
}
2016-09-07 14:10:10.695 KVC[3034:165515] totalNumber--{
    kind = 1;
    new = 3;
    old = 2;
}

以上代码中不涉及移除,根据需要添加代码,对象delloc之前,必须移除。
2、Core Data,自动实现类似的功能。

第二步、注册

1、注册通知
为了能够获取目标属性值改变的通知,需要注册观察者和观察对象属性

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
参数说明:
observer:观察者对象,就是想收到变动通知的对象
keyPath:监测的目标属性的路径
options:决定了通知中内容和发送时间
context:C指针或者对象,传递参数,一般不用传NULL

例如在新建的工程的ViewController中

@property (nonatomic,copy)NSString *name;

- (void)registerAsObserver
{

    [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

}

注意:此方法不持有观察者对象、被观察对象、context,管理好其生命周期。

2、接受通知
当监测的目标对象的属性变化时,观察者将调用observeValueForKeyPath:ofObject:change:context: message,所有的观察者都必须实现此方法

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;
keyPath:监测的目标属性的路径
object:观察者对象
change:变化内容
context:C指针或者对象,传递参数,一般不用传NULL

3、移除通知
当不再使用时,需要通过以下方法移除通知。

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

keyPath:监测的目标属性的路径
observer:观察者对象
context:C指针或者对象,传递参数,一般不用传NULL
以上两个方法,根据需要选择使用。

特别注意:NSArray、NSOrderedSet、NSSet不支持以上三个方法,调用会抛出异常。

第三步、属性变化

使用KVC方法,或者能够触发KVC方法使得监测的目标对象属性变化。

第四步、接收变化

当监测的目标对象的属性变化时,观察者将调用observeValueForKeyPath:ofObject:change:context: message,所有的观察者都必须实现此方法。在此方法中处理变化

以上第二、三、四步组成一次完整的KVO使用过程,下边关于一些参数的用法说明

参数说明

关于NSKeyValueObservingOptions

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
options:决定了通知中内容和发送时间

NSKeyValueObservingOptions是一个枚举类型

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {

    /*通知dic中是否包含新值*/
    NSKeyValueObservingOptionNew = 0x01,

    /*通知dic中是否包含新值*/
    NSKeyValueObservingOptionOld = 0x02,

    /*添加此操作,通知dic中是否包含注册通知前的初始值;如果目标属性是容器类,每个元素都会触发通知发送*/
    NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,

    /*添加此操作,每次值变化,将触发两次:1、变化前(dic中包含NSKeyValueChangeNotificationIsPriorKey,值为1,NSNumber类型) 
    2、变化后
    */
    NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08
};

关于Change Dictionary

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;
change:变化内容

其包含以下几种内容,可以使用以下字段取值

//值变化类型
FOUNDATION_EXPORT NSString *const NSKeyValueChangeKindKey;

//新值
FOUNDATION_EXPORT NSString *const NSKeyValueChangeNewKey;

//旧值
FOUNDATION_EXPORT NSString *const NSKeyValueChangeOldKey;

//容器类中,变化值所在位置,NSIndexSet类型
FOUNDATION_EXPORT NSString *const NSKeyValueChangeIndexesKey;

//是否值变化前,NSNumber类型
FOUNDATION_EXPORT NSString *const NSKeyValueChangeNotificationIsPriorKey NS_AVAILABLE(10_5, 2_0);

其中NSKeyValueChangeKindKey有以下几种类型

typedef NS_ENUM(NSUInteger, NSKeyValueChange) 
{
    NSKeyValueChangeSetting = 1,//值变化
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替换
};

上边的NSKeyValueChangeKindKey2、3、4分别对应着有序集合比如NSArray中增、删、改操作。

参数说明-代码示例

1、NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld

@property (nonatomic,copy)NSString *name;

//注册
- (void)viewDidLoad {
    [super viewDidLoad];

    [self setValue:@"zwq" forKey:@"name"];

    [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

   [self setValue:@"zwq2" forKey:@"name"];

}

//接收
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
    NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
    NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
    NSLog(@"change dic:%@",change);
}

//输出结果
2016-09-06 14:58:55.349 KVC[3614:231751] NSKeyValueChangeKindKey:1
2016-09-06 14:58:55.351 KVC[3614:231751] NSKeyValueChangeOldKey:zwq
2016-09-06 14:58:55.351 KVC[3614:231751] NSKeyValueChangeNewKey:zwq2
2016-09-06 14:58:55.352 KVC[3614:231751] change dic:{
    kind = 1;
    new = zwq2;
    old = zwq;
}

从以上代码可以看出change中的key如何取值,以及其中内容。

2、NSKeyValueObservingOptionInitial

@property (nonatomic,copy)NSString *name;

//注册
- (void)viewDidLoad {
    [super viewDidLoad];

    [self setValue:@"zwq" forKey:@"name"];

    //为了明显观察值变化,多添加两个key
    [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];


    [self setValue:@"zwq2" forKey:@"name"];

}

//接收
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
    NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
    NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
    NSLog(@"change dic:%@",change);
}

//输出结果
2016-09-06 15:06:49.963 KVC[3654:237675] NSKeyValueChangeKindKey:1
2016-09-06 15:06:49.964 KVC[3654:237675] NSKeyValueChangeOldKey:(null)
2016-09-06 15:06:49.964 KVC[3654:237675] NSKeyValueChangeNewKey:zwq
2016-09-06 15:06:49.964 KVC[3654:237675] change dic:{
    kind = 1;
    new = zwq;
}

2016-09-06 15:06:49.964 KVC[3654:237675] NSKeyValueChangeKindKey:1
2016-09-06 15:06:49.965 KVC[3654:237675] NSKeyValueChangeOldKey:zwq
2016-09-06 15:06:49.965 KVC[3654:237675] NSKeyValueChangeNewKey:zwq2
2016-09-06 15:06:49.965 KVC[3654:237675] change dic:{
    kind = 1;
    new = zwq2;
    old = zwq;
}

对比1、2的输出结果,可以看出2中可以获得初始值,后续值变化接收到的通知dic内容同1中一样(可以多改变几次赋值,观察结果)

3、NSKeyValueObservingOptionPrior

@property (nonatomic,copy)NSString *name;

//注册
- (void)viewDidLoad {
    [super viewDidLoad];

    [self setValue:@"zwq" forKey:@"name"];

    //为了明显观察值变化,多添加两个key
    [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionPrior|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];


    [self setValue:@"zwq2" forKey:@"name"];
    [self setValue:@"zwq3" forKey:@"name"];

}

//接收
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
    NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
    NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
    NSLog(@"NSKeyValueChangeNotificationIsPriorKey:%@",change[NSKeyValueChangeNotificationIsPriorKey]);
    NSLog(@"change dic:%@",change);
}

//输出结果
2016-09-06 15:17:16.325 KVC[3730:246941] NSKeyValueChangeKindKey:1
2016-09-06 15:17:16.326 KVC[3730:246941] NSKeyValueChangeOldKey:zwq
2016-09-06 15:17:16.326 KVC[3730:246941] NSKeyValueChangeNewKey:(null)
2016-09-06 15:17:16.326 KVC[3730:246941] NSKeyValueChangeNotificationIsPriorKey:1
2016-09-06 15:17:16.327 KVC[3730:246941] change dic:{
    kind = 1;
    notificationIsPrior = 1;
    old = zwq;
}
2016-09-06 15:17:16.327 KVC[3730:246941] NSKeyValueChangeKindKey:1
2016-09-06 15:17:16.327 KVC[3730:246941] NSKeyValueChangeOldKey:zwq
2016-09-06 15:17:16.327 KVC[3730:246941] NSKeyValueChangeNewKey:zwq2
2016-09-06 15:17:16.327 KVC[3730:246941] NSKeyValueChangeNotificationIsPriorKey:(null)
2016-09-06 15:17:16.328 KVC[3730:246941] change dic:{
    kind = 1;
    new = zwq2;
    old = zwq;
}


2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeKindKey:1
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeOldKey:zwq2
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeNewKey:(null)
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeNotificationIsPriorKey:1
2016-09-06 15:17:16.328 KVC[3730:246941] change dic:{
    kind = 1;
    notificationIsPrior = 1;
    old = zwq2;
}
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeKindKey:1
2016-09-06 15:17:16.329 KVC[3730:246941] NSKeyValueChangeOldKey:zwq2
2016-09-06 15:17:16.329 KVC[3730:246941] NSKeyValueChangeNewKey:zwq3
2016-09-06 15:17:16.329 KVC[3730:246941] NSKeyValueChangeNotificationIsPriorKey:(null)
2016-09-06 15:17:16.329 KVC[3730:246941] change dic:{
    kind = 1;
    new = zwq3;
    old = zwq2;
}

从以上代码输出结果不难看出:a、通知分开发送:变化前和变化后 b、变化前NSKeyValueChangeNewKey为空,但是NSKeyValueChangeNotificationIsPriorKey值为1;变化后反之。

结合以上3段代码结论:4种各有所用,可以单独使用,也可以组合使用,根据需要选择合适。作用简单概括:1.新、旧值 2.初始值 3.值变化前后。

4、NSKeyValueChangeKindKey

@interface ViewController ()

@property (nonatomic,strong)NSArray *datas;
@end

- (void)viewDidLoad {
    [super viewDidLoad];

    /* 创建Data对象 */
    Data * data1 = [[Data alloc] init];
    Data *data2 = [[Data alloc] init];
    Data *data3 = [[Data alloc] init];
    Data *data4 = [[Data alloc] init];


    /* self.datas属性赋值 */
    [self setValue:@[data1,data2,data3] forKey:@"datas"];

    /* 监测self.datas属性 */
    [self addObserver:self forKeyPath:@"datas" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];


    //修改datas数组:增删改
    NSMutableArray *mutable_subdatas = [self mutableArrayValueForKeyPath:@"datas"];
    [mutable_subdatas addObject:data4];
    [mutable_subdatas removeObject:data4];
    [mutable_subdatas replaceObjectAtIndex:2 withObject:data4];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"%@--%@",keyPath,change);
}

//输出结果
2016-09-07 14:46:50.366 KVC[3231:186568] datas--{
    indexes = "<_NSCachedIndexSet: 0x7f8591428c60>[number of indexes: 1 (in 1 ranges), indexes: (3)]";
    kind = 2;
    new =     (
        "<Data: 0x7f8591427b00>"
    );
}


2016-09-07 14:46:50.367 KVC[3231:186568] datas--{
    indexes = "<_NSCachedIndexSet: 0x7f8591428c60>[number of indexes: 1 (in 1 ranges), indexes: (3)]";
    kind = 3;
    old =     (
        "<Data: 0x7f8591427b00>"
    );
}


2016-09-07 14:46:50.367 KVC[3231:186568] datas--{
    indexes = "<_NSCachedIndexSet: 0x7f8591428c40>[number of indexes: 1 (in 1 ranges), indexes: (2)]";
    kind = 4;
    new =     (
        "<Data: 0x7f8591427b00>"
    );
    old =     (
        "<Data: 0x7f8591402350>"
    );
}
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Key-Value Observing (键值监测)