iOS键值观察KVO实例详解

简介

什么是KVO?KVO是Key-Value Observing的简称,翻译成中文就是键值观察。这是iOS支持的一种机制,用来做什么呢?我们在开发应用时经常需要进行通信,比如一个model的某个数据变化了,界面上要进行相应的变化,但是如果我们程序并不知道数据什么时候会进行变化,总不能一直循环判断有没有变化吧,那么就需要在数据变化时给controlller发送一个通知,告知我变化了,你可以更新显示内容了,通知的方式有很多种,比如Notification也是其中一种方式,本文要讲解的KVO也是其中一种很轻巧的方式。

他的实现机制为,为可能改变的数据增加一个观察者,在上面的说法中这个观察者就是controller,它去观察这个数据有没有发生变化,一旦发生变化,就会得到一个信号,从而获取到变化的数据,进行自己要做的操作。

实例效果

image.png

如上图所示,界面上设置两个label,一个显示名字,一个显示分数。还有一个按钮,用来修改分数,现在要做到点击按钮分数变化。

可能你会觉得很简单,直接在按钮的响应方法中将分数的label内容修改不就可以了吗,确实如此,但是这里我们不这么做,而是使用KVO来完成。

我们创建一个学生模型,这个模型有两个属性,一个为姓名,一个为分数。label这是读取模型的数据来进行显示。

现在我们给这个实例化了的学生模型添加一个观察者,定义为我要观察学生模型的分数变化情况,这时,如果这个学生模型的分数发生了变化,比如在按钮响应中只对模型的分数属性进行修改,KVO这个机制就会自动给观察者发送通知,说这个属性变化了,你要做什么操作赶紧做。

于是我们在观察者的KVO回调函数中进行相应的操作,如果我们收到了分数变化的通知,那么就将分数label的值给修改为当前的分数。这样就实现了一套KVO键值观察的流程,当然最后还缺一步就是移除观察者,不过要在确实需要移除的时候再移除,因为移除后就不再会收到变化的通知了。

实现方式

上面例子中进行了一套KVO键值观察的流程,我们整理一下进行了哪些工作:

  1. 设计界面样式
  2. 建立学生模型
  3. 对学生的分数属性添加观察
  4. 修改学生的分数属性
  5. 在观察到变化的响应方法中进行界面更新操作
  6. 不再需要观察的时候移除观察

现在通过这个例子来一步步讲解。

设计样式

样式就不说了,两个label,一个按钮,以及按钮的响应方法,都是很常见的。

建立模型

这个部分,就是新建一个NSObject类,用来作为学生模型,有两个属性:姓名和分数,如下所示:

// StudentModel.h
@interface StudentModel : NSObject

@property (nonatomic, copy) NSString *name;
@property float score;

@end

// ViewController.m
// 在controller中实例化学生模型
self.studentModel = [[StudentModel alloc] init];
[self.studentModel setValue:@"Cloudox" forKey:@"name"];
[self.studentModel setValue:@"89.0" forKey:@"score"];

添加观察

这一步,才是真正开始使用KVO了。

要使用KVO,至少必须要实现两个方法:

  • addObserver:forkeyPath:options:context:
  • observeValueForKeyPath:ofObject:change:context:

第二个方法,就是用来获取数据变化的通知并进行相应操作的方法,这个我们后面再讲,先讲第一个方法,顾名思义,这就是用来添加观察者的方法了。

可能你会注意到,我们上面实例化学生模型的时候,使用的是 setVlue:forKey: 的形式来设置属性值的,为什么要这样设置呢?联想到KVO的名字,键值观察,就能大概明白了,学生模型的属性名就相当于key,属性值就相当于值。

紧接着就可以对分数来添加观察了:

[self.studentModel addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew             context:nil];

这里使用的就是第一个方法,有四个参数。

  • 第一个参数是观察者,这里被观察者是学生模型,观察者是controller,也就是self
  • 第二个参数是keyPath,其实也就是要观察的键
  • 第三个是一个options,这里我们写的是一个枚举值,这个地方可以填几种值,下面在进行详细的说明
  • 第四个我们填了nil,也有其作用,下面再细说

总之通过这行代码,我们就对score这个键,也即是分数添加了观察。

修改数据

在按钮的响应方法中修改学生模型的分数数据,同样使用 setVlue:forKey: 的方式进行设置。

接收通知

这里就用到第二个方法:observeValueForKeyPath:ofObject:change:context:

先看看这个例子中的实现:

// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"score"]) {
        self.scoreLabel.text = [NSString stringWithFormat:@"Score:%@", [self.studentModel valueForKey:@"score"]];
    }
}

可以看到这个回调的变化响应方法也有四个参数,其实就对应上面添加观察时的四个参数,通过keyPath,我们可以判断是不是我们想要接收的数据变化,判断它是不是score,其实也就是对不同的被观察者进行不同的操作。确实是分数变化后,我们就更新界面上的分数label,用新的分数来显示。

移除通知

移除通知的方法很简单,如下:

[self.studentModel removeObserver:self forKeyPath:@"score"];

从观察者那边移除对被观察者特定键的观察。

至此,一个简单的KVO流程就走完了,很简单对吧。

进阶用法

传递对象

上面添加观察者和响应变化的方法中都有一个 context参数,通过这个参数可以传递一些东西,在添加观察者时设置要传递的内容,在响应变化时获得传递的内容。

比如我要传递一个字符串,在添加观察者时设置:

[self.studentModel addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew context:@"heyMe"];// 2.通过context传递内容给观察者

这里在context的参数中就直接设置了要传递的对象字符串。然后在变化响应时:

// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"score"]) {
        self.scoreLabel.text = [NSString stringWithFormat:@"Score:%@", [self.studentModel valueForKey:@"score"]];
    }
    NSLog(@"%@", context);// 通过context获取被观察者传递的内容
}

这里就可以输出context看一下,会得到传递过来的字符串内容。

options参数

在添加观察者时有一个options参数,在回调获取变化时有一个change参数,这两个参数其实是对应的,都是用来增加传递变化的丰富度。

options参数可以设为:

  • NSKeyValueObservingOptionOld:这表示在回调获取变化时可以通过change参数获取变化之前的值;
  • NSKeyValueObservingOptionNew:这表示在回调获取变化时可以通过change参数获取变化后的值;
  • NSKeyValueObservingOptionInitial:在添加观察者方法return的时候就发出一次通知;
  • NSKeyValueObservingOptionPrior:会在观察的值发生变化前发出一次通知,变化后还是会发出一次通知,所以变化一次一共会得到两次通知。

以上就是options参数,可以看到都是对应change参数的,用来决定change参数可以得到什么样的数据,在回调获取变化时可以输出change看一下,就可以知道不同的效果了。

change参数

在使用change的时候可以通过下面的key来操作:

  • NSKeyValueChangeKindKey:对应NSKeyValueChange的枚举值
    • NSKeyValueChangeSetting = 1:说明被观察的数据的setter方法被调用了;
    • NSKeyValueChangeInsertion = 2:当观察的数据是集合时,且对它进行insert操作时会返回该值;
    • NSKeyValueChangeRemoval = 3:当观察的数据是集合时,且对它进行remove操作时会返回该值;
    • NSKeyValueChangeReplacement = 4:当观察的数据是集合时,且对它进行replace操作时会返回该值。
  • NSKeyValueChangeNewKey:对应options参数中的NSKeyValueObservingOptionNew,会在其中包含观察的数据变化后的新值
  • NSKeyValueChangeOldKey:对应options参数中的NSKeyValueObservingOptionOld,会在其中包含观察的数据变化之前得旧值
  • NSKeyValueChangeIndexesKey:当NSKeyValueChangeKindKey是2、3、4的时候,也就是说是观察集合数据时,这个key的值是一个NSIndexSet,包含操作对象的索引集合
  • NSKeyValueChangeNotificationIsPriorKey:包含一个布尔值,如果options的参数是NSKeyValueObservingOptionPrior,也就是会通知两次,在第一次通知,也就是改变前的通知时,会包含这个key

关于这些change的值,都可以输出到控制台试一下看看是什么效果,会有更加直观的感受。

手动通知

之前说的都是自动通知,当添加了观察者后,只要发生改变就会自动通知观察者,但有时候我们并不是什么改变都希望得到通知,或者有时候是希望变化到什么情况后再通知,这就需要改变通知的机制。默认的实现模式为自动通知的模式,要自定义何时进行通知,就要改成手动通知的模式。

要改成手动通知,首先要在被观察者的模型中重写一个方法 automaticallyNotifiesObserversForKey :

// StudentModel.m
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    BOOL automatic = NO;
    if ([key isEqualToString:@"score"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

这里我们在学生模型的实现文件中重写了这个方法,判断当观察的key是score分数时,就将自动通知关闭,其余的情况还是根据父类来进行判断,这样写比较保险。

这样在我们改变学生模型的分数时,就不会自动触发通知了,要触发通知,需要自己进行设置:

// 按钮响应
- (void)changeScore {
    [self willChangeValueForKey:@"score"];// 改为手动通知
    [self.studentModel setValue:@"99.0" forKey:@"score"];
    [self didChangeValueForKey:@"score"];// 改为手动通知
}

这时就可以触发通知了,如果一个操作会触发多个属性改变,都要发通知,那么需要嵌套通知:

// 按钮响应
- (void)changeScore {
    [self willChangeValueForKey:@"name"];// 改为手动通知
    [self willChangeValueForKey:@"score"];// 改为手动通知
    [self.studentModel setValue:@"Cloud" forKey:@"name"];
    [self.studentModel setValue:@"99.0" forKey:@"score"];
    [self didChangeValueForKey:@"score"];// 改为手动通知
    [self didChangeValueForKey:@"name"];// 改为手动通知
}

而在一个一对多的关系中,比如集合,你必须注意不仅仅是这个key改变了,还有它改变的类型以及索引,也就是我们change中对应的几种涉及到集合的东西,如下所示:

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

键值依赖

其实关于KVO还有一个重要的点是键值依赖,也就是说一个属性的值依赖于对象中的其他属性,当那些属性变化后,这个属性的值自动被通知到进行修改,不过这个点没太弄明白,苹果给的例子有点不清不楚的,再研究一下吧。


示例工程:https://github.com/Cloudox/KVODemo


查看作者首页

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

推荐阅读更多精彩内容