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 *)change context:(void *)context{
    NSLog(@"keyPath:%@,object:%@,change:%@,context:%@",keyPath,object,change,context);
}

@end

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

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

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

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

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

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

NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld

那如果只写

NSKeyValueObservingOptionNew

打印出来是这样子的

2019-01-25 14:49:47.024828+0800 KVOtest[97253:905201] keyPath:firstString,
object:,
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:,
change:{
    kind = 1;
    old = "";
},
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 *)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:,
 change:{
    kind = 1;
    old = "";
},
 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:,
 change:{
    kind = 1;
    old = "";
},
 context:(null)
2019-01-25 16:01:12.251331+0800 KVOtest[98377:942808] keyPath:secondString,
 object:,
 change:{
    kind = 1;
    old = "";
},
 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 *)change context:(void *)context{
    NSLog(@"keyPath:%@,\n object:%@,\n change:%@,\n context:%@",keyPath,object,change,context);
}

@end

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

+(NSSet *)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:,
 change:{
    kind = 1;
    new = ">";
},
 context:(null)
2019-01-25 22:45:40.994048+0800 KVOtest[2997:1087322] keyPath:label,
 object:,
 change:{
    kind = 1;
    new = ">";
},
 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:,
 change:{
    kind = 1;
    new = ">";
},
 context:(null)
2019-01-25 23:05:00.652332+0800 KVOtest[3271:1096866] keyPath:label,
 object:,
 change:{
    kind = 1;
    new = ">";
},
 context:(null)

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

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

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

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

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

你可能感兴趣的:(iOS KVO使用)