Objective-C KVC&KVO

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

你可能感兴趣的:(Objective-C,KVO,KVC)