Key-Value Observing (键值监測)

简单介绍

KVO是一套当目标对象的属性值改变时观察者对象能够接受到通知的机制。必须先理解KVC才干更好的理解KVO,前者是后者的实现基础。

这种通信机制在MVC设计模式非常是常见

Key-Value Observing (键值监測)-LMLPHP

实现过程简单来说分为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:@"[email protected]"]; [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

比如在新建的project的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>"
);
}

05-11 19:24