Key-ValueObserving(键值监测)
简介
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)NSStringname;
+(BOOL)automaticallyNotifiesObserversForKey:(NSString)theKey{
BOOLautomatic=NO;
/只自定义指定属性,其它仍然自动发送通知/
if([theKeyisEqualToString:@"name"])
{
//在set方法中手动调用相关方法
automatic=NO;
}
else
{
//此方法默认返回YES
automatic=[superautomaticallyNotifiesObserversForKey:theKey];
}
returnautomatic;
}
-(void)setName:(NSString)name
{
//即将变化
[selfwillChangeValueForKey:@"name"];
_name=name;
//已经变化
[selfdidChangeValueForKey:@"name"];
}
//如果说只有值不相等时才发送通知,提升性能
-(void)setName:(NSString)name
{
if(![nameisEqualToString:_name])
{
[selfwillChangeValueForKey:@"name"];
_name=name;
[selfdidChangeValueForKey:@"name"];
}
}
如果涉及1对多的容器类,需要自己实现NSKeyValueChangeInsertion,NSKeyValueChangeRemoval,NSKeyValueChangeReplacement三种操作对应的方法,例如
//Keys为属性名称
-(void)removeKeysAtIndexes:(NSIndexSet)indexes{
[selfwillChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexesforKey:@"keys"];
//Removethetransactionobjectsatthespecifiedindexes.
[selfdidChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexesforKey:@"keys"];
}
3、属性依赖
如果目标对象属性存在依赖关系,注册合适的依赖Keys。核心方法为
第一种、
+(NSSet)keyPathsForValuesAffectingValueForKey:(NSString)keyNS_AVAILABLE(10_5,2_0);
说明:
1、返回目标属性依赖属性的KeyPath的Set。当对象注册后,KVO自动监测该对象所有的KeyPaths。
2、其默认实现从对象所属类的方法列表中匹配方法:+keyPathsForValuesAffecting(为属性名,比如Name),如果存在执行并返回结果;如果不存在向底层寻找已经废弃的方法+setKeys:triggerChangeNotificationsForDependentKey:
3、可以用来替换手动调用-willChangeValueForKey:/-didChangeValueForKey:来实现属性依赖的解决方案
4、不能在已有类的Category中使用,在Category禁止重写此方法,可以使用+keyPathsForValuesAffecting来实现。
第二种、
或者重写此格式+keyPathsForValuesAffecting(为属性名,比如Name)的方法名
比如说,姓名=姓+名;当二者任一变动时更新姓名
@property(nonatomic,copy)NSStringname;//姓名
@property(nonatomic,copy)NSStringfirstName;//姓
@property(nonatomic,copy)NSStringlastName;//名
//重新get方法,表明字段组成
-(NSString)name
{
return[NSStringstringWithFormat:@"%@%@",_firstName,_lastName];
}
//通过此方法
+(NSSet)keyPathsForValuesAffectingValueForKey:(NSString)key
{
NSSetkeyPaths=[superkeyPathsForValuesAffectingValueForKey:key];
if([keyisEqualToString:@"name"])
{
NSArrayaffectingKeys=@[@"lastName",@"firstName"];
keyPaths=[keyPathssetByAddingObjectsFromArray:affectingKeys];
}
returnkeyPaths;
}
或者
+(NSSet)keyPathsForValuesAffectingName
{
return[NSSetsetWithObjects:@"lastName",@"firstName",nil];
}
//改变值
-(void)viewDidLoad{
[superviewDidLoad];
[selfsetValue:@"张"forKey:@"firstName"];
[selfsetValue:@"三"forKey:@"lastName"];
[selfaddObserver:selfforKeyPath:@"name"options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNewcontext:nil];
//名称改变,刷新姓名
[selfsetValue:@"龙"forKey:@"lastName"];
}
-(void)observeValueForKeyPath:(NSString)keyPathofObject:(id)objectchange:(NSDictionary)changecontext:(void)context
{
NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
}
//输出结果
2016-09-0617:05:01.904KVC[4192:307124]NSKeyValueChangeOldKey:张三
2016-09-0617:05:01.904KVC[4192:307124]NSKeyValueChangeNewKey:张龙
注意:以上关于属性依赖的处理方法不支持一对多的关系。比如说ViewController对象有一个totalNumber表示总数和属性datas数组,数组中Data的对象,对象含有number属性。
/Data对象/
@interfaceData:NSObject
@property(nonatomic,assign)NSIntegernumber;
@end
/ViewController对象/
@interfaceViewController()
@property(nonatomic,assign)NSIntegertotalNumber;
@property(nonatomic,strong)NSArraydatas;
@end
可以采用以下方法解决
1、注册每个Data对象的年龄属性为监测属性,ViewController对象为观察者,data.number变化时,使用集合运算符求和,然后设置ViewController的totalNumber属性值
-(void)viewDidLoad{
[superviewDidLoad];
/创建Data对象/
Datadata1=[[Dataalloc]init];
data1.number=0;
Datadata2=[[Dataalloc]init];
data2.number=0;
Datadata3=[[Dataalloc]init];
data3.number=0;
/self.datas属性赋值/
[selfsetValue:@[data1,data2,data3]forKey:@"datas"];
/监测self.datas中每个data对象的number属性/
//(0,3)中0表示index从0开始,0表示长度3,也就是index(0、1、2);但是不能使得self.datas数组越界
NSIndexSetset=[NSIndexSetindexSetWithIndexesInRange:NSMakeRange(0,3)];
[self.datasaddObserver:selftoObjectsAtIndexes:setforKeyPath:@"number"options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNewcontext:nil];
/监测totalNumber属性/
[selfaddObserver:selfforKeyPath:@"totalNumber"options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNewcontext:nil];
/改变值,查看输出结果/
[data1setValue:@"1"forKey:@"number"];
[data2setValue:@"1"forKey:@"number"];
[data3setValue:@"1"forKey:@"number"];
}
-(void)observeValueForKeyPath:(NSString)keyPathofObject:(id)objectchange:(NSDictionary)changecontext:(void)context
{
if([keyPathisEqualToString:@"number"])
{
[selfupdateTotalNumber];
}
elseif([keyPathisEqualToString:@"totalNumber"])
{
NSLog(@"%@--%@",keyPath,change);
}
}
//更新总数
-(void)updateTotalNumber
{
NSNumbertotal=[selfvalueForKeyPath:@"datas.@sum.number"];
[selfsetValue:totalforKey:@"totalNumber"];
}
//输出结果
2016-09-0714:10:10.694KVC[3034:165515]totalNumber--{
kind=1;
new=1;
old=0;
}
2016-09-0714:10:10.695KVC[3034:165515]totalNumber--{
kind=1;
new=2;
old=1;
}
2016-09-0714:10:10.695KVC[3034:165515]totalNumber--{
kind=1;
new=3;
old=2;
}
以上代码中不涉及移除,根据需要添加代码,对象delloc之前,必须移除。
2、CoreData,自动实现类似的功能。
第二步、注册
1、注册通知
为了能够获取目标属性值改变的通知,需要注册观察者和观察对象属性
-(void)addObserver:(NSObject)observerforKeyPath:(NSString)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullablevoid)context;
参数说明:
observer:观察者对象,就是想收到变动通知的对象
keyPath:监测的目标属性的路径
options:决定了通知中内容和发送时间
context:C指针或者对象,传递参数,一般不用传NULL
例如在新建的工程的ViewController中
@property(nonatomic,copy)NSStringname;
-(void)registerAsObserver
{
[selfaddObserver:selfforKeyPath:@"name"options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNewcontext:nil];
}
注意:此方法不持有观察者对象、被观察对象、context,管理好其生命周期。
2、接受通知
当监测的目标对象的属性变化时,观察者将调用observeValueForKeyPath:ofObject:change:context:message,所有的观察者都必须实现此方法
-(void)observeValueForKeyPath:(nullableNSString)keyPathofObject:(nullableid)objectchange:(nullableNSDictionary)changecontext:(nullablevoid)context;
keyPath:监测的目标属性的路径
object:观察者对象
change:变化内容
context:C指针或者对象,传递参数,一般不用传NULL
3、移除通知
当不再使用时,需要通过以下方法移除通知。
-(void)removeObserver:(NSObject)observerforKeyPath:(NSString)keyPathcontext:(nullablevoid)contextNS_AVAILABLE(10_7,5_0);
-(void)removeObserver:(NSObject)observerforKeyPath:(NSString)keyPath;
keyPath:监测的目标属性的路径
observer:观察者对象
context:C指针或者对象,传递参数,一般不用传NULL
以上两个方法,根据需要选择使用。
特别注意:NSArray、NSOrderedSet、NSSet不支持以上三个方法,调用会抛出异常。
第三步、属性变化
使用KVC方法,或者能够触发KVC方法使得监测的目标对象属性变化。
第四步、接收变化
当监测的目标对象的属性变化时,观察者将调用observeValueForKeyPath:ofObject:change:context:message,所有的观察者都必须实现此方法。在此方法中处理变化
以上第二、三、四步组成一次完整的KVO使用过程,下边关于一些参数的用法说明
参数说明
关于NSKeyValueObservingOptions
-(void)addObserver:(NSObject)observerforKeyPath:(NSString)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullablevoid)context;
options:决定了通知中内容和发送时间
1
2
NSKeyValueObservingOptions是一个枚举类型
typedefNS_OPTIONS(NSUInteger,NSKeyValueObservingOptions){
/通知dic中是否包含新值/
NSKeyValueObservingOptionNew=0x01,
/通知dic中是否包含新值/
NSKeyValueObservingOptionOld=0x02,
/添加此操作,通知dic中是否包含注册通知前的初始值;如果目标属性是容器类,每个元素都会触发通知发送/
NSKeyValueObservingOptionInitialNS_ENUM_AVAILABLE(10_5,2_0)=0x04,
/添加此操作,每次值变化,将触发两次:1、变化前(dic中包含NSKeyValueChangeNotificationIsPriorKey,值为1,NSNumber类型)
2、变化后
/
NSKeyValueObservingOptionPriorNS_ENUM_AVAILABLE(10_5,2_0)=0x08
};
关于ChangeDictionary
-(void)observeValueForKeyPath:(nullableNSString)keyPathofObject:(nullableid)objectchange:(nullableNSDictiowww.shanxiwang.netnary)changecontext:(nullablevoid)context;
change:变化内容
1
2
其包含以下几种内容,可以使用以下字段取值
//值变化类型
FOUNDATION_EXPORTNSStringconstNSKeyValueChangeKindKey;
//新值
FOUNDATION_EXPORTNSStringconstNSKeyValueChangeNewKey;
//旧值
FOUNDATION_EXPORTNSStringconstNSKeyValueChangeOldKey;
//容器类中,变化值所在位置,NSIndexSet类型
FOUNDATION_EXPORTNSStringconstNSKeyValueChangeIndexesKey;
//是否值变化前,NSNumber类型
FOUNDATION_EXPORTNSStringconstNSKeyValueChangeNotificationIsPriorKeyNS_AVAILABLE(10_5,2_0);
其中NSKeyValueChangeKindKey有以下几种类型
typedefNS_ENUM(NSUInteger,NSKeyValueChange)
{
NSKeyValueChangeSetting=1,//值变化
NSKeyValueChangeInsertion=2,//插入
NSKeyValueChangeRemoval=3,//移除
NSKeyValueChangeReplacement=4,//替换
};
上边的NSKeyValueChangeKindKey2、3、4分别对应着有序集合比如NSArray中增、删、改操作。
参数说明-代码示例
1、NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
@property(nonatomic,copy)NSStringname;
//注册
-(void)viewDidLoad{
[superviewDidLoad];
[selfsetValue:@"zwq"forKey:@"name"];
[selfaddObserver:selfforKeyPath:@"name"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:nil];
[selfsetValue:@"zwq2"forKey:@"name"];
}
//接收
-(void)observeValueForKeyPath:(NSString)keyPathofObject:(id)objectchange:(NSDictionary)changecontext:(void)context
{
NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
NSLog(@"changedic:%@",change);
}
//输出结果
2016-09-0614:58:55.349KVC[3614:231751]NSKeyValueChangeKindKey:1
2016-09-0614:58:55.351KVC[3614:231751]NSKeyValueChangeOldKey:zwq
2016-09-0614:58:55.351KVC[3614:231751]NSKeyValueChangeNewKey:zwq2
2016-09-0614:58:55.352KVC[3614:231751]changedic:{
kind=1;
new=zwq2;
old=zwq;
}
从以上代码可以看出change中的key如何取值,以及其中内容。
2、NSKeyValueObservingOptionInitial
@property(nonatomic,copy)NSStringname;
//注册
-(void)viewDidLoad{
[superviewDidLoad];
[selfsetValue:@"zwq"forKey:@"name"];
//为了明显观察值变化,多添加两个key
[selfaddObserver:selfforKeyPath:@"name"options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNewcontext:nil];
[selfsetValue:@"zwq2"forKey:@"name"];
}
//接收
-(void)observeValueForKeyPath:(NSString)keyPathofObject:(id)objectchange:(NSDictionary)changecontext:(void)context
{
NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
NSLog(@"changedic:%@",change);
}
//输出结果
2016-09-0615:06:49.963KVC[3654:237675]NSKeyValueChangeKindKey:1
2016-09-0615:06:49.964KVC[3654:237675]NSKeyValueChangeOldKey:(null)
2016-09-0615:06:49.964KVC[3654:237675]NSKeyValueChangeNewKey:zwq
2016-09-0615:06:49.964KVC[3654:237675]changedic:{
kind=1;
new=zwq;
}
2016-09-0615:06:49.964KVC[3654:237675]NSKeyValueChangeKindKey:1
2016-09-0615:06:49.965KVC[3654:237675]NSKeyValueChangeOldKey:zwq
2016-09-0615:06:49.965KVC[3654:237675]NSKeyValueChangeNewKey:zwq2
2016-09-0615:06:49.965KVC[3654:237675]changedic:{
kind=1;
new=zwq2;
old=zwq;
}
对比1、2的输出结果,可以看出2中可以获得初始值,后续值变化接收到的通知dic内容同1中一样(可以多改变几次赋值,观察结果)
3、NSKeyValueObservingOptionPrior
@property(nonatomic,copy)NSStringname;
//注册
-(void)viewDidLoad{
[superviewDidLoad];
[selfsetValue:@"zwq"forKey:@"name"];
//为了明显观察值变化,多添加两个key
[selfaddObserver:selfforKeyPath:@"name"options:NSKeyValueObservingOptionPrior|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNewcontext:nil];
[selfsetValue:@"zwq2"forKey:@"name"];
[selfsetValue:@"zwq3"forKey:@"name"];
}
//接收
-(void)observeValueForKeyPath:(NSString)keyPathofObject:(id)objectchange:(NSDictionary)changecontext:(void)context
{
NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
NSLog(@"NSKeyValueChangeNotificationIsPriorKey:%@",change[NSKeyValueChangeNotificationIsPriorKey]);
NSLog(@"changedic:%@",change);
}
//输出结果
2016-09-0615:17:16.325KVC[3730:246941]NSKeyValueChangeKindKey:1
2016-09-0615:17:16.326KVC[3730:246941]NSKeyValueChangeOldKey:zwq
2016-09-0615:17:16.326KVC[3730:246941]NSKeyValueChangeNewKey:(null)
2016-09-0615:17:16.326KVC[3730:246941]NSKeyValueChangeNotificationIsPriorKey:1
2016-09-0615:17:16.327KVC[3730:246941]changedic:{
kind=1;
notificationIsPrior=1;
old=zwq;
}
2016-09-0615:17:16.327KVC[3730:246941]NSKeyValueChangeKindKey:1
2016-09-0615:17:16.327KVC[3730:246941]NSKeyValueChangeOldKey:zwq
2016-09-0615:17:16.327KVC[3730:246941]NSKeyValueChangeNewKey:zwq2
2016-09-0615:17:16.327KVC[3730:246941]NSKeyValueChangeNotificationIsPriorKey:(null)
2016-09-0615:17:16.328KVC[3730:246941]changedic:{
kind=1;
new=zwq2;
old=zwq;
}
2016-09-0615:17:16.328KVC[3730:246941]NSKeyValueChangeKindKey:1
2016-09-0615:17:16.328KVC[3730:246941]NSKeyValueChangeOldKey:zwq2
2016-09-0615:17:16.328KVC[3730:246941]NSKeyValueChangeNewKey:(null)
2016-09-0615:17:16.328KVC[3730:246941]NSKeyValueChangeNotificationIsPriorKey:1
2016-09-0615:17:16.328KVC[3730:246941]changedic:{
kind=1;
notificationIsPrior=1;
old=zwq2;
}
2016-09-0615:17:16.328KVC[3730:246941]NSKeyValueChangeKindKey:1
2016-09-0615:17:16.329KVC[3730:246941]NSKeyValueChangeOldKey:zwq2
2016-09-0615:17:16.329KVC[3730:246941]NSKeyValueChangeNewKey:zwq3
2016-09-0615:17:16.329KVC[3730:246941]NSKeyValueChangeNotificationIsPriorKey:(null)
2016-09-0615:17:16.329KVC[3730:246941]changedic:{
kind=1;
new=zwq3;
old=zwq2;
}
从以上代码输出结果不难看出:a、通知分开发送:变化前和变化后b、变化前NSKeyValueChangeNewKey为空,但是NSKeyValueChangeNotificationIsPriorKey值为1;变化后反之。
结合以上3段代码结论:4种各有所用,可以单独使用,也可以组合使用,根据需要选择合适。作用简单概括:1.新、旧值2.初始值3.值变化前后。
4、NSKeyValueChangeKindKey
@interfaceViewController()
@property(nonatomic,strong)NSArraydatas;
@end
-(void)viewDidLoad{
[superviewDidLoad];
/创建Data对象/
Datadata1=[[Dataalloc]init];
Datadata2=[[Dataalloc]init];
Datadata3=[[Dataalloc]init];
Datadata4=[[Dataalloc]init];
/self.datas属性赋值/
[selfsetValue:@[data1,data2,data3]forKey:@"datas"];
/监测self.datas属性/
[selfaddObserver:selfforKeyPath:@"datas"options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNewcontext:nil];
//修改datas数组:增删改
NSMutableArraymutable_subdatas=[selfmutableArrayValueForKeyPath:@"datas"];
[mutable_subdatasaddObject:data4];
[mutable_subdatasremoveObject:data4];
[mutable_subdatasreplaceObjectAtIndex:2withObject:data4];
}
-(void)observeValueForKeyPath:(NSString)keyPathofObject:(id)objectchange:(NSDictionary)changecontext:(void)context
{
NSLog(@"%@--%@",keyPath,change);
}
//输出结果
2016-09-0714:46:50.366KVC[3231:186568]datas--{
indexes="<_NSCachedIndexSet:0x7f8591428c60>[numberofindexes:1(in1ranges),indexes:(3)]";
kind=2;
new=(
""
);
}
2016-09-0714:46:50.367KVC[3231:186568]datas--{
indexes="<_NSCachedIndexSet:0x7f8591428c60>[numberofindexes:1(in1ranges),indexes:(3)]";
kind=3;
old=(
""
);
}
2016-09-0714:46:50.367KVC[3231:186568]datas--{
indexes="<_NSCachedIndexSet:0x7f8591428c40>[numberofindexes:1(in1ranges),indexes:(2)]";
kind=4;
new=(
""
);
old=(
""
);
}
|
|