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对象,存储了发生变化的集合元素的索引值。
NSKeyValueChangeOldKey,NSKeyValueChangeNewKey 对应了的是数组,里面存储了相关变量的原来的值,变化后的值,还有所发生的变化。
一个实现该方法的例子:
- (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 Notification和Manual 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
相关文章推荐
- Object.prototype.toString应用和原理探析
- Objective-C内存管理
- Objective-C block (块)
- Objective-C集合
- Objective-C之NSNumber
- Programming with Objective-C——翻译2章
- DOM和JQUERY 对HTML标记修改的冲突之 swfobject
- Objective - C类的扩展
- Objective-C 协议 protocol
- CCObject的分析:release retain基于2.2.3 增加3.2ref对比
- objc_setAssociatedObject 使用
- velocity和ruby和object-c的关联
- iOS:Xcode注释的用法---Objective C
- object-c和jre和jquery的关联
- framework和object-c和jquery的关联
- struts2和dos和object-c的关联
- Swift开发第九篇——Any和AnyObject&typealias和泛型接口
- object-c和jre和wireshark的关联
- jquery和object-c和c++的关联
- object-c和windows和samba的关联