KVO

KVO 简介

KVO 键值观察机制,就是观察指定对象的指定属性变化情况。

KVO 键值观察 依赖于 KVC 健值编码

Key-value observing 通常用于 MVC 中,model 与 controller直接的通讯。

继承于 NSObject 才可以拥有 KVO 机制

两种 KVO 应用方式

  • 自动 KVO
  • 手动 KVO

举个例子,一个 Person 类, 一个 Account

  • [图1]

Account 的属性 余额 balance 改变时通知 Person
Account 添加观察者 Person
[account addObserver:person forKeyPath:@"balance" options:options context:context];

  • [图2]

Person 观察 Account 的属性变化
Person 中实现观察者方法 observeValueForKeyPath:ofObject:change:context:

  • [图3]

Account 移除观察者 Person
[account removeObserver:person forKeyPath:@"balance" context:context]

  • [图4]

这篇文章的重要内容

  1. Registering for Key-Value Observing 注册键值观察的过程

  2. Registering Dependent Keys 对于存在 key 依赖的键值观察

  3. Key-Value Observing Implementation Details 键值观察的实现细节


1. Registering for Key-Value Observing 注册键值观察的过程

KVO 生命周期的过程,必须完成下面这三个方法

  1. 给被观察者注册观察者 addObserver:forKeyPath:options:context:.
  2. 在观察者里实现方法 接受通知 observeValueForKeyPath:ofObject:change:context:
  3. 移除观察者 removeObserver:forKeyPath: 要在观察者的内存销毁之前 移除观察者机制

- 注册观察者 addObserver:forKeyPath:options:context:.

  • Options
    Options 影响方法 observeValueForKeyPath:ofObject:change:context: 中的 change 字典

    • NSKeyValueObservingOptionOld: change 包含 old value
    • NSKeyValueObservingOptionNew: 'change' 包含 new value
    • NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew: 即包含 old 也包含 new
    • NSKeyValueObservingOptionInitial : change 中不包含 key 的值,会在 kvo 注册的时候立即发送通知。
    • NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew : 注册kvo时立即发送通知 change 中有 new 值,这里的 new 值是注册之前 key 的值。
    • NSKeyValueObservingOptionPrior : 会在值发生改变前发出一次通知,当然改变后的通知依旧还会发出,也就是每次change都会有两个通知。值变化之前发送通知的 change 中包含一个键值对 NSKeyValueChangeNotificationIsPriorKey:@(1), 值发生变化之后的的通知 change 不包含上面提到的 键值对, 可以跟 willChange 手动通知搭配使用
  • Context
    是一个 void * 指针,可以传任意数据进去,可以防止父类跟子类同时对同一个 key 注册观察者造成的异常。

    可以采用下面的方法创建一个 Context

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

    命名规范:static void * 类名+属性名+Context

- Receiving Notification of a Change 接收通知响应

        所有的观察者必须实现方法 `observeValueForKeyPath:ofObject:change:context: message.`
  • keyPath : 注册 KVO 时的 keyPath
  • object : 被观察者对象
  • change : 根据注册 KVO 时 option 不同而展示不同的内容,change中可能会包含 keyPath 变化前后的值,对应的键有
    • NSKeyValueChangeOldKey
    • NSKeyValueChangeNewKey

keyPath是个标量或者 c 语言结构体,会把这些包装成 NSNumberNSValue,

keyPath 是个容器, change中可能会包含这个容器 inserted,removed,replaced 的情况 对应的键有

- `NSKeyValueChangeInsertion`
- `NSKeyValueChangeRemoval` 
- `NSKeyValueChangeReplacement`

keypath 是个 NSIndexSet : change 中可能会包含数组

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

- Removing an Object as an Observer 移除观察者

观察者被移除之后就不会再接受到通知。

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

需要注意的地方

  • 不要重复移除观察者,会造成异常,如果不知道这个观察者是否已经被移除,可以在 try/catch 安全移除观察者
  • 在观察者内存销毁之前从观察者中释放出来,不然会造成内存异常
  • 无法检测一个对象是否处于观察者模式中,
    • init 或者 viewDidLoad 中添加观察者
    • dealloc 中移除观察者

Automatic Change Notification 自动 KVO 通知

NSObject 提供了自动健值更改通知的实现,自动 KVO 通知依赖于 KVC 编码机制获取, KVC method,和 集合代理(collection proxy)mutableArrayValueForKey:

手动 KVO

当你想要控制整个 KVO 的进程可以采用手动 KVO, 比如减少不必要的通知,比如将大量的通知控制到一个通知中。
手动 KVO 跟 自动 KVO 可以共存,比如同一个对象的同一个属性,可以在父类里自动 KVO, 在子类里手动 KVO, 重写方法 automaticallyNotifiesObserversForKey即可

  • 打开手动 KVO 的开关 automaticallyNotifiesObserversForKey

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    
       BOOL automatic = NO;
       if ([theKey isEqualToString:@"balance"]) {
           automatic = NO;
       }
       else {
           automatic = [super automaticallyNotifiesObserversForKey:theKey];
       }
       return automatic;
    }
    
    • 手动触发 KVO(一般是在 set 方法里)
      • willChangeValueForKey
      • didChangeValueForKey
      - (void)setBalance:(double)theBalance {
          if (theBalance != _balance) {
              [self willChangeValueForKey:@"balance"];
              _balance = theBalance;
              [self didChangeValueForKey:@"balance"];
          }
      }
      
    • 也可以把多个触发 KVO 的 key 放到一起
      - (void)setBalance:(double)theBalance {
          [self willChangeValueForKey:@"balance"];
          [self willChangeValueForKey:@"itemChanged"];
          _balance = theBalance;
          _itemChanged = _itemChanged+1;
          [self didChangeValueForKey:@"itemChanged"];
          [self didChangeValueForKey:@"balance"];
      }
      
    • 容器内部更改时,采用采用手动 KVO(当容器内容修改时,会触发 KVO)
      注意:根据容器变化的类型去设置对应的 NSKeyValueChange
      • NSKeyValueChangeInsertion
      • NSKeyValueChangeRemoval
      • NSKeyValueChangeReplacement

      针对容器对象的 KVO ,需要借用 KVC 机制创建的 容器代理。

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

2.KVO - 注册依赖 key

单个关系

重写下面两个方法中的一个即可

  • keyPathsForValuesAffectingValueForKey

  • keyPathsForValuesAffecting<Key>
    用例如下

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

多个关系

比如,有个部门类 Department,有个员工类 Employees, 每个员工的薪水都会影响这个部门的总薪水。
可以在 Department 添加、删除 Employees 的时候,给 Employees 注册、移除观察者 Employees

用例如下

```
- (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;
}
```

3. KVO 内部实现细节

使用 isa-swizzling 原理
当 对象的属性注册到观察者中时,会创建一个中间类,重写了被观察属性的 setter 方法。

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

推荐阅读更多精彩内容

  • KVO编程指南 Key-Value Observing Programming Guide 1 Introduct...
    codeTao阅读 571评论 0 0
  • 引言 键值观察(KVO)提供了一种机制以允许对象被告知其他对象的特定属性的更改,它对应用程序中的模型和控制器层之间...
    渐z阅读 539评论 0 0
  • 本文结构如下: Why? (为什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等开会阅读 1,610评论 1 21
  • 本文由我们团队的 纠结伦 童鞋撰写。 文章结构如下: Why? (为什么要用KVO) What? (KVO是什么...
    知识小集阅读 7,381评论 7 105
  • 三十二 宸妃 自赵嫔晋位后,皇帝的心渐渐回到了苏妃身上,对苏妃多加宠爱和怜惜。 而苏妃仍旧淡淡的,不骄不躁。 君后...
    君清兮阅读 160评论 0 0