iOS开发之——KVO

引子

KVO:即Key-Value-Observer,键值观测模式,它是一种允许当某些对象的特定属性值改变时,及时通知给对象的观察者(其他对象)的机制。

观察者的注册和移除

KVO的大致流程包括:给要监听的属性所属的类添加观察者;接收到属性改变的通知后进行处理;处理完之后接触观察者三大步骤。流程很简单,就像要把大象装进冰箱总共需几步类似。其中,一对一代表对非集合类的属性监听,一对多代表对集合类的属性监听。

注册

注册方法:

[person addObserver:observer
             forKeyPath:@"age"
                options:NSKeyValueObservingOptionNew
                context:NULL];

各个参数的意义很明了。分别依次是:被观察类的实例对象,观察者类的实例对象,被观察的属性名称,观察选项,额外参数

注册选项

注册选项包括四个,它们的名字和效果依次是:

  • NSKeyValueObservingOptionNew:通知观察者属性发生变化时的新值;
  • NSKeyValueObservingOptionOld:通知观察者属性发生变化时之前的旧值;
  • NSKeyValueObservingOptionInitial:在注册观察者方法(addObserver:observer)未返回时就会开始发送通知,因为被观察者的初始化(initial value)对于观察者来说也是新变化的值;
  • NSKeyValueObservingOptionPrior:发送两条通知,也就是当属性值即将要发生变化时,即下边要说的willChangeValueForKey触发时间相对应,预先发送给观察者一条通知,待属性值改变之后跟上述三个选项一样还会发出通知。

通知的处理

观察者收到通知后,需要通过特定的方法进行处理,样例代码:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context
{
    NSLog(@"%@", change);
    if ([keyPath isEqualToString:@"name"])
    {
        //通知事件的处理
        NSLog(@"%@的名字发生了变化!%@", object, change);
    }
}

这个方法一定要在观察者的类中进行重写。

通知信息

通知处理方法中的change:(NSDictionary<NSString *,id> *)change是一个字典类型的对象,包含了此次变化的信息,例如NSKeyValueObservingOptionNew选项下,一对一的属性发生变化时接收到的变化信息如下:

{
    kind = 1;
    new = hexintao;
}

第一条的kindNSKeyValueChange类型的枚举值,它有如下四个定义:

  • NSKeyValueChangeSetting:设置新值,被监听的是一对一的属性或者一对多的属性;
  • NSKeyValueChangeInsertion:一对多的属性新插入了一个对象;
  • NSKeyValueChangeRemoval:一对多的属性移除了一个对象;
  • NSKeyValueChangeReplacement:一对多的属性替换了其中的某个对象。

第二条则会根据NSKeyValueObservingOptions观察选项、一对一或者一对多的属性不同而不同。大致也就是显示设置的新值、变化之前的旧值或者是一对多属性的添加、移除、替换的对象和序号(index)等。具体的信息可以command+observeValueForKeyPath,查看通知处理方法的官方注释,写的非常详细。

移除

待属性值发生变化的通知处理完毕之后,我们需要对注册的观察者进行手动解除,解除的方法是:

- (void)removeObserver:(NSObject *)anObserver
            forKeyPath:(NSString *)keyPath

- (void)removeObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               context:(void *)context

对没有没有进行监听的属性(keyPath)执行解除操作,会抛出异常。同样,如果对已经注册的监听属性没有执行解除操作,也会抛出异常。

自动通知和手动通知

如果按照以上操作步骤执行,则默认使用的是自动通知,即只要对属性值进行重新赋值(不管新值和旧值是否相同),观察者都会收到通知。而在实际应用中,有可能我们想根据自己的需要,待属性值满足我们的条件之后才给观察者发送通知,这时候我们就需要通过手动模式修改发送通知的条件和时间来达到目的了。

自动通知1

如上述操作,发送通知的时机和条件无法进行修改。

//第一次修改可以正常接收到通知
[person setValue:@"hexintao" forKey:@"name"];
//自动通知模式下,接下来这两次依然会接收到通知
[person setValue:@"hexintao" forKey:@"name"];
[person setValue:@"hexintao" forKey:@"name"];

当然,实际应用中,连续赋同样的值的情况不多见也不推荐,我们只是为了以此说明自动通知模式下的情况。

手动通知

首先需要关闭自动通知,在被观察者的类中重写类方法:
(2017.01.04修改:如果不重写这个类方法,则系统会在属性 setter 方法的之前之后自动调用 willChangeValueForKeydidChangeValueForKey,造成同一次属性的修改调用两次 KVO 监听方法。)

+ (BOOL)automaticallyNotifiesObserversOf<Key>
{
    return NO;
}

或者是:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = YES;
    if ([key isEqualToString:@"name"])
    {
        automatic = NO;
    }
    else
    {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
}

后一个方法较复杂,而且有把属性名称(key)拼错的风险,所以还是推荐使用第一种方法。
接下来,要在需要发出通知的地方手动调用两个方法,这个例子中我们就取属性值的setter方法:

- (void)setName:(NSString *)name
{
    if (name != _name)
    {
        //子类不能重写这两个方法,否则无法完成手动触发KVO
        //通过改变这两个方法的位置,可以自定义KVO触发的条件
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
}

上述两个方法一定要成对调用才会成功发出通知。
刚才那个例子:

//第一次修改可以正常接收到通知
[person setValue:@"hexintao" forKey:@"name"];
//手动通知模式下,接下来这两次不会接收到通知
[person setValue:@"hexintao" forKey:@"name"];
[person setValue:@"hexintao" forKey:@"name"];

依赖键的注册

有时候,我们监听的某个属性值可能会依赖于其他多个属性,只要其他属性发生了改变都会导致我们监听的属性发生变化,这种就叫做依赖键。例如,在Person类中有一个personInfo的属性,它返回的是对象的nameage的组合:

- (NSString *)personInfo
{
    return [NSString stringWithFormat:@"person's name:%@ age:%d", self.name, self.age];
}

如果我们对personInfo进行监听,则nameage的变化也会导致personInfo发生变化,这时候我们就需要设置依赖键。

+ (NSSet *)keyPathsForValuesAffectingPersonInfo
{
    return [NSSet setWithObjects:@"name", @"age", nil];
}

然后再对personInfo进行注册监听,之后如果我们对name或者age的值进行修改的时候,观察者就会收到这样的通知:

CollectionViewTest[742:17933] {
    kind = 1;
    new = "person's name:hexintao age:0";
}

也就是当关联属性中的任何一个发生了变化,我们监听的这个属性就会收到通知,说明其值发生了变化。

集合属性的监听

集合属性整体还是部分?

一对一属性的监听相对来说比较简单,只要值发生了变化我们收到通知进行处理即可。对于一对多集合类的属性来说,牵扯到是监听整个集合发生的变化还是其中元素的变化?这两种行为都可以通过KVO监听到,不过日常使用来说,我们更倾向于监听后者。
对于集合属性,正常的添加或删除对象的操作并不能触发KVO,例如[person.personFriends addObject:@"2in"],观察者并不会收到变化通知,不过对于集合属性整体的改变,例如person.personFriends = [[NSMutableArray alloc]init],观察者可以正常收到通知。不过我们重点讨论通过特定的方法监听集合属性中对象的变化。大致分为两种方法:

手动监听

集合属性的手动监听即:在被监听的类中重写一些修改集合元素的方法,之后调用这些方法对属性进行修改就可以触发KVO监听。
我们可以根据需要实现:

//插入对象
- (void)insertObject:(id)object in<Key>AtIndex:(NSUInteger)index
//移除对象
-(void)removeObjectFrom<Key>AtIndex:(NSUInteger)index

具体的操作元素的方法可见官方手册:KVC官方指导
之后我们通过调用重写后的方法修改集合属性时即可触发KVO。

自动监听

自动监听大致是:通过mutableArrayValueForKey方法获得一个可变对象的代理,对其进行修改即可自动触发KVO。而valueForKey返回的则是不可变对象。
使用样例:

[[person mutableArrayValueForKey:@"personFriends"] addObject:@"3in"];

// 但是如果是将上述操作赋值给一个可变数组,再调用正常的类似于addObject方法将不会触发KVO监听。
NSMutableArray *friends = [person mutableArrayValueForKey:@"personFriends"];
//不能触发KVO模式
[person.personFriends addObject:@"4in"];  

//这两个方法能触发KVO,但是friends和person.personFriends指向的并不是同一个对象,不过其内容却完全一样,
//对friends操作会影响到person.personFriends的值,反过来也是如此!
[friends insertObject:@"5in" atIndex:0];  //可以触发KVO
[friends removeObjectAtIndex:0];  //可以触发KVO

参考资料

Apple KVO官方手册
NSHipster KVO
可变对象在KVO中的监听
KVO详解

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

推荐阅读更多精彩内容

  • KVO:(Key-Value-Observer)键值观察者,是观察者设计模式的一种具体实现。 在我们编程中,很多时...
    邦奇诺阅读 237评论 0 2
  • 本质(更新于 2019年02月26日23:17:19): 首先,KVO是观察者模式的一种实现其次,Apple使用了...
    helloDolin阅读 1,149评论 5 4
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 2,983评论 0 26
  • 本文结构如下: Why? (为什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等开会阅读 1,609评论 1 21
  • 么么哒
    老毛zoo阅读 112评论 4 0