ios开发资源收集iOS一些实用的东西程序员

iOS KVO(键值观察) 总览

2016-04-07  本文已影响4500人  yahtzee_

原文链接 Cyrus'blog

本文主要内容来自于对官方文档 Key-Value Observing Programming Guide 的翻译,以及一部分我自己的理解和解释,如果有说错的地方请及时联系我。

At a Glance

KVO 也就是 键值观察 ,它提供了一种机制,使得当某个对象特定的属性发生改变时能够通知到别的对象。这经常用于 model 和 controller 之间的通信。KVO主要的优点是你不需要在每次属性改变时手动去发送通知。并且它支持为一个属性注册多个观察者。

注册 KVO

注册成为观察者


为了能够在属性改变时被通知到,一个 观察者对象 必须通过 被观察对象addObserver:forKeyPath:options:context: 方法注册成为观察者。

注意: addObserver:forKeyPath:options:context: 方法不会持有观察者对象,被观察对象,以及context的强引用。你要确保自己持有了他们的强引用。

属性变化时接收通知


当一个被观察属性的值发生改变时,观察者会收到 observeValueForKeyPath:ofObject:change:context: 的消息。所有的观察者必须实现这个方法。这个方法中的参数和注册观察者方法的参数基本相同,只有一个 change 不同。 change 是一个字典,它里面包含了的信息由注册时的 options 决定。

官方提供了这些key给我们来取到 change 中的value:

NSString *const NSKeyValueChangeKindKey;
NSString *const NSKeyValueChangeNewKey;
NSString *const NSKeyValueChangeOldKey;
NSString *const NSKeyValueChangeIndexesKey;
NSString *const NSKeyValueChangeNotificationIsPriorKey;
enum {
  NSKeyValueChangeSetting = 1,
  NSKeyValueChangeInsertion = 2,
  NSKeyValueChangeRemoval = 3,
  NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

change[NSKeyValueChangeKindKey]NSKeyValueChangeSetting 的时候,说明被观察属性的setter方法被调用了。
而下面三种,根据官方文档的意思是,当被观察属性是集合类型,且对它进行了 insert,remove,replace 操作的时候会返回这三种Key,但是我自己测试的时候没有测试出来😓不知道是不是我理解错了。

移除一个观察者


你可以通过 removeObserver:forKeyPath: 方法来移除一个观察。如果你的 context 是一个 对象,你必须在移除观察之前持有它的强引用。当移除了观察后,观察者对象再也不会受到这个 keyPath 的通知。

KVO Compliance

有两种方式能够保证 change notification 能够被发出。

自动通知


NSObject 已经实现了自动通知,只要通过 setter 方法去赋值,或者通过 KVC 就可以通知到观察者。自动通知也支持集合代理对象,比如 mutableArrayValueForKey: 方法。

// Call the accessor method.
[account setName:@"Savings"];

// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];

// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];

// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

手动通知


手动通知提供了更自由的方式去决定什么时间,什么方式去通知观察者。这可以帮助你最少限度触发不必要的通知,或者一组改变值发出一个通知。想要使用手动通知必须实现automaticallyNotifiesObserversForKey: 方法。(或者automaticallyNotifiesObserversOfS<Key>)在一个类中同时使用自动和手动通知是可行的。对于想要手动通知的属性,可以根据它的keyPath返回NO,而其对于其他位置的keyPath,要返回父类的这个方法。

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

要实现手动通知,你需要在值改变前调用 willChangeValueForKey: 方法,在值改变后调用 didChangeValueForKey: 方法。你可以在发送通知前检查值是否改变,如果没有改变就不发送通知

- (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"];
}

在一个一对多的关系中,你必须注意不仅仅是这个key改变了,还有它改变的类型以及索引。

- (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"];
}

键之间的依赖

在很多种情况下一个属性的值依赖于在其他对象中的属性。如果一个依赖属性的值改变了,这个属性也需要被通知到。

To-one Relationships


比如有一个教 fullName 的属性,依赖于 firstNamelastName,当 firstName 或者 lastName 改变时,这个 fullName 属性需要被通知到。

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

你可以重写 keyPathsForValuesAffectingValueForKey: 方法。其中要先调父类的这个方法拿到一个set,再做接下来的操作。

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

你也可以通过实现 keyPathsForValuesAffecting<Key> 方法来达到前面同样的效果,这里的<Key>就是属性名,不过第一个字母要大写,用前面的例子来说就是这样:

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

To-many Relationships


keyPathsForValuesAffectingValueForKey:方法不能支持 to-many 的关系。举个例子,比如你有一个 Department 对象,和很多个 Employee 对象。而 Employee 有一个 salary 属性。你可能希望 Department 对象有一个 totalSalary 的属性,依赖于所有的 Employee 的 salary 。

你可以注册 Department 成为所有 Employee 的观察者。当 Employee 被添加或者被移除时,你必须要添加和移除观察者。然后在 observeValueForKeyPath:ofObject:change:context: 方法中,根据改变做出反馈。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

KVO的实现细节

KVO 的实现用了一种叫 isa-swizzling 的技术。isa 指针就是指向类的指针,当一个对象的一个属性注册了观察者后,被观察对象的isa指针的就指向了一个系统为我们生成的中间类,而不是我们自己创建的类。在这个类中,系统为我们重写了被观察属性的setter方法。你可以通过 object_getClass(id obj) 方法获得对象真实的类,在 addObserver 前后分别打印,就可以看到isa指针被指向了一个中间类。似乎都是在原来的类名前面加上 NSKVONotifying_

isa指针不总是指向真实的类,所以你不应该依赖于 isa 指针来判断这个对象的类型,而应该通过 class 方法来判断对象的类型。如果你还不知道什么是isa指针,可以看我之前写的博客 Objective-C runtime 的简单理解与使用

上一篇下一篇

猜你喜欢

热点阅读