iOS KVO使用

简述

KVO是key-value-observe的简称,也就是键值观察者,是一种设计模式 -- 观察者模式。核心思想就是:

被观察者的状态发生改变时,会通知给观察者,观察者在对应的方法里可以获取相关信息。

KVO要完整实现,需要四个要素即:观察者、被观察者、被观察的“变量”以及实现回调方法。
当然,KVO的实现不是死板的,可以通过三种途径实现即:自动KVO、手动KVO以及依赖键KVO。下面通过实例来一个个讲解。

一、准备工作

首先 创建新项目,然后创建一个SecondModel类,并添加name和age两个属性

@interface SecondModel : NSObject

@property(nonatomic, strong)NSString *name;
@property(nonatomic, assign)NSInteger age;

@end

第二步 在ViewController类中引入SecondModel类的头文件,并为ViewController类的class-continuation分类中添加一个SecondModel类型的属性和一个字符串属性。

#import "ViewController.h"
#import "SecondModel.h"
@interface ViewController ()

@property (nonatomic, strong)SecondModel *model;
@property (nonatomic, strong)NSString *firstString;

@end

第三步 了解两个方法。对于上面说的三种KVO,都会用到两个方法,一个是添加观察者的方法,另一个是删除观察者的方法,介绍如下:

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath 
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;

参数介绍

observer -- 观察者
keyPath -- 被观察者的“变量”,通常就是被观察对象的属性
options -- 在回调方法中我需要获取到的“变量”值,如新值(NSKeyValueObservingOptionNew)、旧值(NSKeyValueObservingOptionOld)等。
context -- 上下文,我理解的是观察者所在的上下文,用于区分有很多观察者时,进行分类处理。传nil也可以。

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

或者

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

参数介绍

observer
keyPath
context
三个参数应该连起来讲:被观察者 移除在指定上下文(context)中,其被观察“变量”名为keyPath的观察者(observer)。

二、自动KVO

所谓自动是相对于手动KVO来说的,先用代码实现,后面再说自动手动。
下面根据KVO实现的四要素来完成一个KVO实例
观察者:对象model,
被观察者:ViewController的实例对象,
被观察的“变量”:firstString,
先完成这个三个要素,所以在ViewController类的viewDidLoad中实现代码如下:

[self addObserver:self.model forKeyPath:@"firstString" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];

添加了观察者,那一定要添加移除观察者的代码,养成好习惯,不添加会导致内存泄漏

-(void)dealloc{
    [self removeObserver:self.model forKeyPath:@"firstString"];
}

那最后一个要素就是“实现回调方法”,那应该在那里实现呢?谁作为观察者,就在谁那里实现。所以这里应该在SecondModel类里面实现。

#import "SecondModel.h"

@implementation SecondModel

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"keyPath:%@,object:%@,change:%@,context:%@",keyPath,object,change,context);
}

@end

好,到现在条件都已具备,只差触发了,只要改变firstString的值就可以触发。我在ViewController类里面实现touchesBegan:方法

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.firstString = @"you are beautiful";
}

运行项目,点击屏幕,打印如下:

2019-01-25 14:29:16.415324+0800 KVOtest[96959:895162] keyPath:firstString,
object:<ViewController: 0x7fc8f960fec0>,
change:{
    kind = 1;
    new = "you are beautiful";
    old = "<null>";
},
context:(null)

这样就可以清楚的看到从回调方法中获取到的数据,根据你的需要进行取舍。

这里多说下这个options,前面添加观察者的时候,写的代码是:

NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld

那如果只写

NSKeyValueObservingOptionNew

打印出来是这样子的

2019-01-25 14:49:47.024828+0800 KVOtest[97253:905201] keyPath:firstString,
object:<ViewController: 0x7f873f708c50>,
change:{
    kind = 1;
    new = "you are beautiful";
},
context:(null)

很明显change里面没有old字段了。因为这次options 参数填的是NSKeyValueObservingOptionNew
如果只写

NSKeyValueObservingOptionOld

打印结果如下

2019-01-25 14:53:18.271560+0800 KVOtest[97324:907276] keyPath:firstString,
object:<ViewController: 0x7ff6d0d0ea30>,
change:{
    kind = 1;
    old = "<null>";
},
context:(null)

这么看的话,也就可以得出结论,填什么option就会返回对应值,值得注意的是,两个option之间用“|”隔开。

自动KVO的解释

自动KVO -- 指的是,在类中添加属性时是自动生成对应getter/setter方法的,那么观察这种属性就是属于自动KVO。上面的实例就是自动KVO。

三、手动KVO

手动KVO其实就是关掉自动KVO,然后自己手动绑定KVO。实现流程如下。
首先 和上面一样,添加一个属性、添加观察者、移除观察者
在ViewController的.m文件中添加代码如下:

@property (nonatomic, strong)NSString *secondString;
[self addObserver:self.model forKeyPath:@"secondString" options:(NSKeyValueObservingOptionOld) context:nil];
[self removeObserver:self.model forKeyPath:@"secondString"];

因为之前已经实现了观察者的回调方法,所以,这里就省略了。
然后 关闭自动KVO,实现代码如下

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"secondString"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

这个方法是系统提供的,在调用addObserver:forkeyPath:options:context:方法时,就会走到这个方法里面,当对应的keyPath 进到这个方法时,返回NO,那就是关闭自动KVO;同时注意,除此之外的所有keyPath 都让它走父类的方法,这样就不会影响其他的KVO。
到这一步的话,不出意外,当我改变secondString的值时,是不会走
observeValueForKeyPath:...的回调方法的,验证下。
改变secondString的值,如下

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.firstString = @"you are beautiful";
    self.secondString = @"white color";
}

打印如下:

2019-01-25 15:56:31.392765+0800 KVOtest[98301:939948] keyPath:firstString,
 object:<ViewController: 0x7fe84c415eb0>,
 change:{
    kind = 1;
    old = "<null>";
},
 context:(null)
(lldb) 

并没有secondString相关的信息,对应的自动KVO确实被关闭了。
第三 实现secondString的setter方法,同时一前一后调用changeValueForKey的两个方法,代码如下:

-(void)setSecondString:(NSString *)secondString{
    [self willChangeValueForKey:@"secondString"];
    _secondString = secondString;
    [self didChangeValueForKey:@"secondString"];
}

注意,两个方法都要调用,缺一不可,相当于自己手动绑定KVO。
现在再运行一次,触发touchBegin方法,打印如下:

2019-01-25 16:01:10.751904+0800 KVOtest[98377:942808] keyPath:firstString,
 object:<ViewController: 0x7f8a8b409320>,
 change:{
    kind = 1;
    old = "<null>";
},
 context:(null)
2019-01-25 16:01:12.251331+0800 KVOtest[98377:942808] keyPath:secondString,
 object:<ViewController: 0x7f8a8b409320>,
 change:{
    kind = 1;
    old = "<null>";
},
 context:(null)

这样手动KVO就实现了。虽然我也不知道手动KVO有什么好。

依赖键KVO

想象有这么一个场景,一个视图label的显示值是由model的某个属性值决定,即:

label.text = model.a

那么当modela 改变时,通常我们会再执行一遍上面的那一行代码,来给label重新赋值,假如当modela值改变的时机比较多是,那代码就会有多处出现上面的那行代码,虽然问题不大,但是使用依赖键KVO可以缓解这个问题。

依赖建KVO在这里可以这么解释:
添加一个观察者,观察label,当model.a的值发生改变时,通过依赖键(即label依赖键“model.a”),label.text的值也会发生改变,同时观察者的回调方法会执行。
下面用一个实例来解释:
首先创建一个MessageLabel类,继承自UILabel。在ViewController中添加一个MessageLabel属性

@property (nonatomic, strong)MessageLabel *label;

并初始化

self.label = [[MessageLabel alloc] initWithFrame:CGRectMake(40, 100, [UIScreen mainScreen].bounds.size.width - 80, 50)];
self.label.backgroundColor = [UIColor cyanColor];
[self.view addSubview:self.label];

然后 与之前的步骤一样添加观察者、删除观察者

[self addObserver:self.label forKeyPath:@"label" options:(NSKeyValueObservingOptionNew) context:nil];
-(void)dealloc{
    [self removeObserver:self.model forKeyPath:@"firstString"];
    [self removeObserver:self.model forKeyPath:@"secondString"];
    [self removeObserver:self.label forKeyPath:@"label"];
}

同时在MessageLabel类中添加回调方法

#import "MessageLabel.h"

@implementation MessageLabel

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"keyPath:%@,\n object:%@,\n change:%@,\n context:%@",keyPath,object,change,context);
}

@end

第三 设置键的依赖关系
设置键之间的依赖关系,有两种方式,先看第一种实现方式,代码如下:

+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    if ([key isEqualToString:@"label"]) {
        return [NSSet setWithObjects:@"model.name", @"model.age", nil];
    }
    return [super keyPathsForValuesAffectingValueForKey:key];
}

当添加一个被观察的keyPath,这个方法就会走。其大概意思是就是:用一组键的值去影响另一个键值。而上面的代码的意思就是,当添加的keyPath是label,那么将model.name和model.age的键作为键label的依赖,那么当model.name或model.age对应的值中任何一个发生改变,都会走MessageLabel中观察者的回调方法,就相当于直接改变label的值,也会走MessageLabel中观察者的回调方法一样。

注意 该方法一定要考虑非指定key时的情况要调用
[super keyPathsForValuesAffectingValueForKey:key],不能影响其他的情况

验证下,在touchBegin:方法中添加如下两行代码

    self.model.name = @"逻辑";
    self.model.age = 18;

运行点击屏幕触发touchbegin:方法,得到的打印结果如下

2019-01-25 22:45:38.746935+0800 KVOtest[2997:1087322] keyPath:label,
 object:<ViewController: 0x7fdd6240b430>,
 change:{
    kind = 1;
    new = "<MessageLabel: 0x7fdd62609790; baseClass = UILabel; frame = (40 100; 334 50); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600001405130>>";
},
 context:(null)
2019-01-25 22:45:40.994048+0800 KVOtest[2997:1087322] keyPath:label,
 object:<ViewController: 0x7fdd6240b430>,
 change:{
    kind = 1;
    new = "<MessageLabel: 0x7fdd62609790; baseClass = UILabel; frame = (40 100; 334 50); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600001405130>>";
},
 context:(null)

可以看到走了两次回调方法,是达到了预期结果。不过看不到label的改变,因为添加了依赖键,但是值并没有关联。这里就需要实现label的getter方法了:

-(MessageLabel *)label{
    _label.text = [NSString stringWithFormat:@"姓名:%@,年龄:%ld",self.model.name,self.model.age];
    return _label;
}

再运行,触发touchBegin方法,打印如下:

2019-01-25 23:05:00.652017+0800 KVOtest[3271:1096866] keyPath:label,
 object:<ViewController: 0x7f9c5c41dea0>,
 change:{
    kind = 1;
    new = "<MessageLabel: 0x7f9c5c41f0d0; baseClass = UILabel; frame = (40 100; 334 50); text = '\U59d3\U540d\Uff1a\U903b\U8f91\Uff0c\U5e74\U9f84\Uff1a0'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000014d0eb0>>";
},
 context:(null)
2019-01-25 23:05:00.652332+0800 KVOtest[3271:1096866] keyPath:label,
 object:<ViewController: 0x7f9c5c41dea0>,
 change:{
    kind = 1;
    new = "<MessageLabel: 0x7f9c5c41f0d0; baseClass = UILabel; frame = (40 100; 334 50); text = '\U59d3\U540d\Uff1a\U903b\U8f91\Uff0c\U5e74\U9f84\Uff1a18'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000014d0eb0>>";
},
 context:(null)

可以看到每次model.name 和model.age的改变都会使label的text属性值也会发生改变。
其实到现在,就完成了依赖键KVO。每次model的name或者age的值改变时,视图层label的text也会跟着变化,不需要每次为label的text赋值。
上面说了有两种设置依赖关系的方式,那现在说说这个第二种方式。
其实也是实现一个方法,只不过这个方法更有针对性,如下代码:

+(NSSet<NSString *> *)keyPathsForValuesAffectingLabel{
    return [NSSet setWithObjects:@"model.name", @"model.age", nil];
}

可以看到,这个方法是专门针对label属性设置依赖关系的。其实每个属性都有一个这样的方法,当给类添加属性时,都会自动生成一个对应的方法。这种实现看起来更简洁易懂,效果是一样的。
总结一下依赖键KVO如下

  • 调用添加观察者的方法;
  • 调用删除观察者方法;
  • 实现观察者回调方法;
  • 实现keyPathsForValuesAffecting<key>方法设置依赖键;
  • 实现key的getter方法,并将依赖键值与key对应的值关联。
结语

到此KVO使用介绍完毕,文章中有些地方是为了方便自己理解,表述上不太规范,如有不妥或错误处欢迎你的指出,谢谢!

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

推荐阅读更多精彩内容