您的位置:首页 > 移动开发 > Objective-C

Objective-C KVC&KVO

2016-03-19 18:51 351 查看

Objective-C KVC&KVO

- KVC(Key - Value Coding,键值编码)

使用属性名或属性路径来访问类的属性。

key,就是@”属性名”

keyPath,就是属性的路径,@”属性名.属性名“

什么意思呢?

已知一个类,定义了属性
NSString *name
和一个结构体变量person(person中有一个变量为age)。

我们假设这个类有个对象是p;

那么我们要访问p的变量name,可以用@“name”

[p valueForKey:@“name”];


要访问p的变量person,当然也可以用@“person”,那么如果要访问person结构体变量中的age呢,可以用@“person.age”

[p valueForKeyPath:@“person.age”];


就是这么简单!

以上两个是相当于getter,当然也有相当于setter的方法:

(void)setValue:(id)value forKey:(NSString *)key;

(void)setValue:(id)value forKeyPath:(NSString *)keyPath;

从上面可以看出,这里把key看做属性名,而value就作为属性值。由此可以延伸到另外一个很灵活的方法。

我们知道
NSDictionary
就是存储键值对的,如果可以把NSDictionary的key作为这里的key,把NSDictionary的value作为这里的value,就可以实现一次性给对象的多个属性赋值了!

确实有这样的方法。

(void)setValuesForKeysWithDictionary:(NSDictionary*)keyAndValues;

新建字典的方法不多说,注意把@“属性名”对应放在字典的key位置,把要赋给属性的值(或对象)对应放在字典的value位置:

NSDictionary *keyAndValues=@[@“属性1”:对象1,@“属性2”:对象2,   …   ,@“属性n”:对象n]


这里需要注意,如果要访问的属性实际上是基本类型,而不是对象,则通过key来访问获得的是一个NSValue对象,属性值就封装在里面。对于下面的KVO也是一样。

使用KVC技术是有前提的,比如这个属性要有默认的setter方法set<属性名>,等等这里不细说。可以参见:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-SW1

- KVO(Key - Value Observing)

将某个对象和另外一个对象的属性关联起来,当一个属性(被监视者)变化的时候,会通知另外一个属性(监视器)。

NSObject类已经实现了KVO,因此可以说所有的Cocoa对象都继承了KVO的功能。

KVO里面,一个属性被监视(Observed),一个属性是监听器(Observer),要KVO功能正常发挥作用的前提是这两者都能够支持KVO。

1. 对于被监视者,需要为其添加监视者

添加监视者方法:
addObserver:forKeyPath:options:context:


比如:为account对象的属性openingBalance注册一个监听器inspector,并(通过options)指定需要带上这个属性的原来的值和新的值。

account addObserver:inspector forKeyPath:@“openingBalance" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];


context是指通知中附带的消息(类型为指针),这里选择了不传送附带消息。

2. 对于监听器,则需要接收通知

每当监视的属性有变化,监听器都都用这个方法,因此所有的监听器都需要实现这个方法
observeValueForKeyPath:ofObject:change:context:


这里KeyPath和KVC中的用法一样

ofObject声明了KeyPath是相对哪个对象的

change是一个dictionary对象,用来存储有关变化的详细信息

context和上面注册监听器的方法里的参数context对应。

那么如何访问这个dictionary对象以获得有关变化的信息呢?

可以通过这些入口:也就是key

NSKeyValueChangeKindKey 对应一个NSNumber对象,它的值对应了枚举类型NSKeyValueChange中的某一个值,这个枚举类型对所发生的变化划分了几个种类。

NSKeyValueChangeIndexesKey 对应一个NSIndexSet对象,存储了发生变化的集合元素的索引值。

NSKeyValueChangeOldKeyNSKeyValueChangeNewKey 对应了的是数组,里面存储了相关变量的原来的值,变化后的值,还有所发生的变化。

一个实现该方法的例子:

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqual:@"openingBalance"]) {
_balance= [change objectForKey:NSKeyValueChangeNewKey];
}
/*
如果父类实现了这个方法的话,记得要调用一下父类的这个方法
NSObject没有实现这个方法,如果父类是NSObject的话不需要调用下面这段。
*/
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}


实际上,在被监听的属性发生变化以后,除了会执行上面实现了的方法,还会将上面方法中被改变的对象曾经参与过的动作都重新执行一遍。比如,在openingBalance发生变化之前,就曾执行过
NSLog(@“%@”,inspector.balance);
,那么在openingBalance发生变化以后,自动会再次输出inspector.balance的值,而不需要额外添加代码,也就是说,有一个自动更新的机制在里面。而且,这些更新的操作是紧接着被监听对象的改变之后执行的,只有执行完这些操作才会去执行,修改被监听对象的语句之后的语句。

还要注意,被监听的属性如果是个类的对象,那么通过change查询到的就是这个对象,如果被监听的属性是C的基本类型,或者是标量(如NSInteger),返回的则是封装了这个量的NSValue对象。

3. 取消监听

- (void)unregisterForChangeNotification {
[observedObject removeObserver:inspector forKeyPath:@"openingBalance"];
}


4. 被监听者需要发送变化消息

变化发生后,触发通知的方式有两种:Automatic Change NotificationManual Change Notification(自动通知和手动通知)。

自动通知是由NSObject提供的,所以所有遵守KVC条件的子类都具有这个能力。

也就是,一调用setter方法,或者通过KVC方法来改变属性,就会触发变化通知,自动的。

手动通知:需要实现手动通知的类必须重写NSObject的自动通知方法,也就是automaticallyNotifiesObserversForKey:方法。

重写这个方法的目的是确定某个属性是使用自动通知还是手动通知:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}


这上面的方法(注意是类方法哦!),指定只有openingBalance变化的时候才使用手动通知,其他的属性采用自动通知。返回值automatic就表明了是否采用自动通知。

在手动通知的情况下,要触发通知,需要在改变属性之前调用
willChangeValueForKey:
方法改变之后调用
didChangeValueForKey:
方法,比如:

- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];

_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}


为了避免不必要的通知,通常在修改一个变量之前,最好判断一下值有没有变:

- (void)setOpeningBalance:(double)theBalance {
if (theBalance != _openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}


如果一个动作会引起多个属性变化的话,要这么写:

- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}


如果改变的是一对多的关系,具体来说,就是去改变类型为集合的属性,比如改变集合内部的对象,那么要怎么去触发通知呢:

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}


可见,是与willChangeValueForKey和didChangeValueForKey相对的,要改变集合的元素,需要调用的是
willChange:valueAtIndexes:forKey:
didChange:valueAtIndexes:forKey:
。这两个方法和前面两个的区别在于,多了两个参数:变化种类,和要改变的元素的下标。

这里的变化种类,就会存到在通知中讲到的change字典中与key“NSKeyValueChangeKindKey”对应的NSNumber对象中;这里要改变的元素的下标,就存到与key“NSKeyValueChangeIndexesKey”对应的NSIndexSet对象里面。

要改变的种类,也就是之前说到的枚举类型,有3个值:NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 和NSKeyValueChangeReplacement。

5. 一个对象监听多个对象(属性)

上面都是一个对象监听另外一个对象,在实际运用中,也有很多情况是一个监听多个对象,比如说,一个人的全名,由姓和名组成,那么全名这个对象,就需要同时监听姓对象和名对象。

这种情况下,不能用
observeValueForKeyPath:ofObject:change:context:
方法了,因为这个方法只能为调用者添加一个监听者。我们需要实现另外一个方法,为调用者添加多个被监听的对象:

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}


上面的参数key就是对其他属性的依赖者,返回值是一个NSSet(不允许重复元素),里面存储的就是依赖者要依赖的多个对象的key。其中先用NSArray来承接,显然是防止key有重复,然后再把NSArray的元素添加到由父类方法返回的NSSet中。

从代码中还可以看出,这个方法不单单针对fullName这个属性,全部需要添加多个被监听对象的依赖者都应该在这里得到处理。主要是通过if语句来区别不同的依赖者。

当然我们也可以单独地为每个依赖者写一个方法,而不是一起写。要求是方法的命名需要遵循一定的原则:
keyPathsForValuesAffecting<Key>
,这里的Key就是依赖者的属性名。比如对于fullName,我们可以实现方法

keyPathsForValuesAffectingFullName,方法的实现简单多了:
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}


(一种特殊情况是,如果依赖者是定义在一个类的category里面的话,就不能重写keyPathsForValuesAffectingValueForKey:方法了,因为不能够重写category的方法。此时,就可以用单独的实现方法
keyPathsForValuesAffecting<Key>
来实现。)

那么和前面遇到的情况类似,如果被监听对象是某个集合的元素的话,而在上面的方法中只支持key,不支持keyPath,要怎么办呢?有两种方法可以解决这个问题:

累了,用到再看:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVODependentKeys.html#//apple_ref/doc/uid/20002179-BAJEAIEE
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: