Key-Value Observing

官方链接.

Introduction

Key-value observing(KVO) 是一种机制,它允许对象在 更改其他对象的指定属性时 得到通知。

At a Glance

在应用中在 modelcontroller 层间通讯非常有用(在 OS X中, controllerbinding 技术严重依赖 KVO), controller 同时观察 model 的属性, view 通过 controller 观察 model 的属性. 另外, model 可能观察其他 model 甚至是 model 本身(通常由依赖值决定).

可以观察包括简单的属性, to-one and to-many 关系. to-many 的观察者被通知 change 的类型就像改变涉及的值一样.
简单例子, 假设 Person 有一个 Account 属性, 表现为 person 保存 account 到银行. Person 实例可能需要知道 Account 实例的某些方面何时发生更改,比如余额或利率。
如果 Account 的属性时公有属性, 则 Person 可以通过轮询查看 changes, 这种方式显然低效, 更好的方法是使用 KVO, 在 change 时, 类似 Person 接收一个分岔.
使用 KVO, 必须确定被观察对象, Account 是否符合. 通常, 如果对象继承 NSObject, 使用一般方式创建的属性, 都自动符合 KVO, 也可以手动实现遵从性. KVO Compliance 描述了自动和手动 KVO 以及怎样实现.
下一步, 必须注册观察者实例, 例如 Person, 被观察者 Account, Person 发送一个 addObserver:forKeyPath:options:context: 消息给 Account, 为每个被观察的 key-path,并将自己命名为观察者。

image.png

为了接收 Account 改变的消息, Person 实现 observeValueForKeyPath:ofObject:change:context: 方法, 每个观察者都必须实现. Account 在注册的 key-path 发生改变时发送消息给 Person. Person 作相应处理

image.png

最终, 当它不想收到消息, 至少要在它析构之前 Person 实例必须通过发送 removeObserver:forKeyPath: 消息给 Account 注销.

image.png

Registering for Key-Value Observing 描述了 KVO 从注册到注销的生命周期.
KVO 主要好处就是不需要实现自己的方案来在每次属性更改时发送通知. 具有良好的基础设置具有框架级的支持, 使其便于采用--通常你不需要想工程中添加任何代码. 此外,基础设施已经功能齐全,这使得支持单个属性的多个观察者以及相关值变得很容易。
Registering Dependent Keys 解释了如何定义一个 key 依赖另一个 key.
不像 NSNotificationCenter, 它没有中心对象给所有观察者提供 change 消息. 当发生 change 时,消息直接被发送给观察的对象, 这是 NSObject 提供 KVO 的基本实现.

Key-Value Observing Implementation Details KVO 是如何实现的.

Registering for Key-Value Observing

必须执行以下步骤以让一个对象可以接收遵循 KVO 属性的 KVO 消息:

  • addObserver:forKeyPath:options:context: 方法给被观察者注册观察者.
  • 在观察者内部实现 observeValueForKeyPath:ofObject:change:context: 方法接收通知消息.
  • 使用 removeObserver:forKeyPath: 注销观察者当它不想收到消息使, 最低限度, 在观察者从内存被释放之前调用.

并非所有类的属性都符合 KVO. KVO Compliance中描述如何确定类是否符合 KVO, 通常苹果提供的 frameworks 中的属性只有在被文档化时才符合 KVO.

Registering as an Observer

addObserver:forKeyPath:options:context::

  • Options
    选项参数指定为位或选项常量,它影响通知中提供的更改字典的内容,以及生成通知的方式。
    NSKeyValueObservingOptionOld(change 前旧值) |
    NSKeyValueObservingOptionNew(change 后新值) |
    NSKeyValueObservingOptionInitial( addObserver:forKeyPath:options:context: 方法返回前) |
    NSKeyValueObservingOptionPrior(在属性 change 之前的通知, 普通通知在其后, 通过 NSKeyValueChangeNotificationIsPriorKey 获取值为 NSNumber 转换为 YES, 当观察者自身的 KVO遵从性要求它为其依赖于所观察属性的属性调用 -willChange… 之一时, 一般的通知来得太晚, 无法及时调用 willChange...)

  • Context
    context 指针会在观察者对应的消息中传递. 可以指定为 NULL 也可以完全依赖于 keypath 字符串决定通知的初始化. 但是这会导致对象父类(也实现了一个相同 keypath 的观察)一些不明原因的问题.
    一个更安全可扩展的方法就是通过 context 保证通知是你指定的观察者而不是父类.
    类中唯一命名的静态变量的地址是一个很好的上下文。可以为整个类选择上下文, 并依赖 keypath:

static void * PersonAccountBalanceContext =&PersonAccountBalanceContext;
static void * PersonAccountInterestRateContext =&PersonAccountInterestRateContext;

将自身注册为 Account 实例 balanceinterestRate 属性的观察者:

- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}

addObserver:forKeyPath:options:context: 方法不保持对观察者,被观察对象或上下文的强引用。您应该确保在必要时保持对观察者,被观察对象和上下文的强引用。

Receiving Notification of a Change

当对象的属性发生变化时, 观察者会接收到 observeValueForKeyPath:ofObject:change:context: 消息.
change 字典 key NSKeyValueChangeKindKey 包含发生改变的信息. 如果被观察的值发生变化, NSKeyValueChangeKindKey 返回 NSKeyValueChangeSetting. NSKeyValueChangeOldKey and NSKeyValueChangeNewKey 包含属性的变化的前后值. 如果属性是一个对象, 值直接被提供, 如果是一个标量或一个 C 结构体, 会转换为 NSValue 对象(as KVC).
如果被观察对象是一个 to-many 的关系, NSKeyValueChangeKindKey 会标识是NSKeyValueChangeInsertion(插入), NSKeyValueChangeRemoval(删除), or NSKeyValueChangeReplacement(替换).
NSKeyValueChangeIndexesKey 是一个 NSIndexSet 对象指定变化的关系索引.
如果注册观察者时指定 NSKeyValueObservingOptionNew or NSKeyValueObservingOptionOld options, 则 change 字典中 NSKeyValueChangeOldKey and NSKeyValueChangeNewKey 返回关联对象改变前/后 的值数组.

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}

注意: 如果通知传播到类层次结构的顶部,则 NSObject 抛出一个 NSInternalInconsistencyException 因为这是一个编程错误:子类无法使用它注册的通知。

Removing an Object as an Observer

- (void)unregisterAsObserverForAccount:(Account*)account {
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

移除观察者记住以下几点:

  • 移除一个未注册的观察者会导致 NSRangeException. 只需要为对应于 addObserver:forKeyPath:options:context: 调用一次 removeObserver:forKeyPath:context:, 如果不可行, 则可以将 removeObserver:forKeyPath:context: 放到 try/catch block 中处理潜在的异常。
  • 当观察者析构时不会自动移除. 被观察对象继续发送消息, 像发送一个消息给已经释放的对象, 触发 exception.
  • 无法判断一个对象是否是观察者或被观察对象. 避免这种情况. 典型的方式是在观察者初始化的时候注册(init or viewDidLoad), 在对象析构时注销(dealloc), 确保正确配对和排序添加和删除消息, 观察者在释放之前注销.

KVO Compliance

遵循 KVO, 类必须确保:

  • 指定的属性遵循 KVC, KVO 同 KVC 一样支持相同的数据类型.
  • 由类为属性变化通知.
  • 相关 key 已正确注册.

NSObject 默认自动支持类遵循 KVC 的属性.
子类通过实现 automaticallyNotifiesObserversForKey: 方法控制是否自动发消息.

Automatic Change Notification

NSObject 提供自动键值更改通知的基本实现:

//调用访问器方法。
[account setName:@"Savings"];
 
//使用 setValue:forKey:。
[account setValue:@"Savings" forKey:@"name"];
 
//使用 key-path,其中'account'是'document' 遵循 KVC 的属性。
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
//使用 mutableArrayValueForKey: 检索 关系代理对象。
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

Manual Change Notification

手动通知, 控制指定的 key 发送 change 通知.

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

手动通知需要在属性值发生改变前后分别调用 willChangeValueForKey:didChangeValueForKey::
手动调用 balance 属性的 KVO.

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}

判断属性是否有变化, 是否需要发送通知:

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

一个操作改变多个值:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}

有序集的情况下, 必须指定不仅仅是 key 改变, 还有对象调用的改变类型以及索引.变化类型是一个 NSKeyValueChange 值, 索引是 NSIndexSet 对象.
有序集的删除操作:

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

Registering Dependent Keys

在许多情况下,一个属性的值取决于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生了变化,那么派生属性的值也应该标记为变化。如何确保为这些依赖属性发布 KVO 通知取决于关系的基数。

To-One Relationships

触发 to-one 关系对象自动发出消息, 需要重写 keyPathsForValuesAffectingValueForKey: 方法或适合的方法,该方法遵循它为注册依赖键定义的模式。
例如, fullName 依赖 first and last name, 获取 fullName 的方法可能为:

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

观察 fullName 的对象, 不管 firstName 还是 lastName 属性发生改变时都应该被通知到, 作为他们影响的值.
一个解决办法是通过重写 keyPathsForValuesAffectingValueForKey: 方法指定 fullName 属性依赖 firstNamelastName 属性:

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

也可以实现一个形如 keyPathForValuesAffecting<Key>的类方法:

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

不能通过分类形式给一个已经存在的类重写 keyPathsForValuesAffectingValueForKey: 方法, 但可以实现 keyPathsForValuesAffecting<Key> 类方法.

To-Many Relationships

keyPathsForValuesAffectingValueForKey: 方法不支持 to-many 关系对象, 比如有一个 Department 对象有一个 to-many 关系对象(employees)包含 Employee, Employee 有一个 salary 属性. 你可能想要 Department 对象有一个 totalSalary 属性统计所有 Employeessalaries. 使用 keyPathsForValuesAffectingTotalSalary 返回 employees.salary 是不行的.
有两种解决方法:

  1. 可以通过注册父类对象(Department)作为观察者, 观察所有子属性(Employees). 在关系对象中(employees)中添加或移除子对象时, 必须响应的添加和移除父对象观察者. 在 observeValueForKeyPath:ofObject:change:context: 方法中根据 changes 更新依赖值:

    - (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;
    }
    
  2. 如果是 Core Data, 则可以通过 NSNotificationCenter 注册通知.

Key-Value Observing Implementation Details (KVO 的实现细节)

KVO 的实现使用的是 isa-swizzling 技术.
isa 指向维护在 dispatch table 中的类的对象. dispatch table 中包含类实现的方法以及其他数据.
当一个对象的属性注册了一个观察者, 被观察对象的 isa 会被修改, 指向一个中间类而不是真正的类. 因此,isa 指针的值不一定反映实例的实际类。
永远不要使用 isa 来判断类的从属关系. 应该使用 class 方法.

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

推荐阅读更多精彩内容