iOS KVO(键值观察) 总览

原文链接 Cyrus'blog

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

At a Glance

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

注册 KVO

  • 被观察对象 的属性必须是 [KVO Compliant](file:///Users/hcy/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-BAJEAIEE)
  • 必须用 被观察对象addObserver:forKeyPath:options:context: 方法注册观察者
  • 观察者 必须实现 observeValueForKeyPath:ofObject:change:context: 方法

注册成为观察者


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

  • observer 参数也就是一个观察者对象

  • keyPath 表示要观察的属性

  • options 决定了提供给观察者change字典中的具体信息有哪些。(change字典是一个提供给观察者的参数,后面会提到)

    • NSKeyValueObservingOptionOld 表示在change字典中包含了改变前的值。
    • NSKeyValueObservingOptionNew 表示在change字典中包含新的值。
    • NSKeyValueObservingOptionInitial 在注册观察者的方法return的时候就会发出一次通知。
    • NSKeyValueObservingOptionPrior 会在值发生改变前发出一次通知,当然改变后的通知依旧还会发出,也就是每次change都会有两个通知。
  • context 这个参数可以是一个 C指针,也可以是一个 对象引用,它可以作为这个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;
  • NSKeyValueChangeKindKey 这个key包含的value是一个 NSNumber 里面是一个 int,与之对应的是 NSKeyValueChange 的枚举
enum {
  NSKeyValueChangeSetting = 1,
  NSKeyValueChangeInsertion = 2,
  NSKeyValueChangeRemoval = 3,
  NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

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

  • NSKeyValueChangeNewKeyNSKeyValueChangeOldKey 顾名思义,当你在注册的时候 options 参数中填了对应的 NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld ,并且 NSKeyValueChangeKindKey 的值是 NSKeyValueChangeSetting ,你就可以通过这两个key取到 旧值和新值。

  • NSKeyValueChangeIndexesKey, 当 NSKeyValueChangeKindKey 的结果是 NSKeyValueChangeInsertion, NSKeyValueChangeRemovalNSKeyValueChangeReplacement 的时候,这个key的value是一个NSIndexSet,包含了发生insert,remove,replace的对象的索引集合

  • NSKeyValueChangeNotificationIsPriorKey,这个key包含了一个 NSNumber,里面是一个布尔值,如果在注册时 options 中有 NSKeyValueObservingOptionPrior,那么在前一个通知中的 change 中就会有这个key的value, 我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES;

移除一个观察者


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

KVO Compliance

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

  • 自动通知,继承自NSObject,并且所有的属性符合[KVC规范](file:///Users/hcy/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/Compliant.html#//apple_ref/doc/uid/20002172)这样就不用写额外的代码去实现自动通知。
  • 手动通知,让你的子类实现 automaticallyNotifiesObserversForKey: 方法,来决定是否需要自动通知,如果是手动通知需要额外的代码。

自动通知


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 的简单理解与使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容