介绍
工程中我们常常需要得到成员变量
或属性
的值的改变, 在iOS开发中:
成员变量
或属性
指对象的参数, 如: 一个人的名字:person.name
-
成员变量或属性
的成员变量或属性
指对象的参数的参数, 如: 一个人的孩子的名字:person.child.name
如我们需要实时得到某个用户的信用情况, 针对不同的信用等级, 我们有不同的操作. 我们定个属性:
user.credit
: 当
user.credit == great
, 圣诞节到了, 我们给他送个礼物当
user.credit == good
, 我们提升这个用户的信用额度当
user.credit == ok
, 我们给他打个标签: 优质用户当
user.credit == bad
, 我们关闭他的借款权限
在上述情况下, 我们可以使用Cocoa
提供给我们的KVO(Key-value observing)
来实现:
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
KVO
也体现了在iOS
开发中常使用的一种设计模式 - 观察者设计模式.
KVO 的使用
步骤
- 添加监听:
addObserver: forKeyPath: options: context:
- 实现监听方法:
observeValueForKeyPath: ofObject: change: context:
- 移除监听:
removeObserver: forKeyPath:
示例
- 在
ViewController
创建一个属性
@property (nonatomic, copy) NSString *name;
- 添加
key-value-observer
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
- 实现监听值(此处为
name
)变化时的监听方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"name = %@", self.name);
}
}
- 在
ViewController
的dealloc
中移除
- (void)dealloc{
[self removeObserver:self forKeyPath:@"name"];
}
- 注: 移除
observer
视实际情况而定, 也可以在viewDidDisappear:
或者处理完监听, 在observeValueForKeyPath: ofObject: change: context:
最后.
测试
- 在
viewController
中添加一个title
为更改name
的按钮, 为其添加一个事件, 用来修改name
, 如下:
static NSInteger idx;
///修改name
- (IBAction)modifyNameAction:(id)sender {
NSArray *nameArr = @[@"张三", @"李四", @"王五", @"赵六", @"Jim七", @"David八", @"Kevin九", @"Danny十"];
self.name = nameArr[idx];
idx++;
if (idx > 9) {
idx = 0;
}
}
- 注: 此处为了使代码紧凑, 未优化
nameArr
- 点击按钮, 更改
name
属性, 可以看到KVO
的监听方法被触发:
以上即为KVO
的基本使用, 也是系统的自动调用
. KVO自动调用
的原理为:
- 系统会重写被监听属性的
setter
方法, 如上述的setName:
, 所以, 必须监听属性, 有setter
方法 - 系统会依次调用:
- 1)- willChangeValueForKey:
- 2)
setter
方法 - 3)- didChangeValueForKey:
- 4)通知观察者.
这也解释了NSKeyValueObservingOptionOld(旧值)
及NSKeyValueObservingOptionNew(新值)
的来源.
验证:
重写setter
, willChangeValueForKey:
, didChangeValueForKey:
- (void)setName:(NSString *)name{
NSLog(@"22---setter");
_name = name;
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"11---will key = %@", key);
}
- (void)didChangeValueForKey:(NSString *)key{
[super didChangeValueForKey:key];
NSLog(@"33--- did key = %@", key);
}
观察打印如下:
- 有自动调用, 就有手动调用, 手动调用我们将在后面讲述.
监听一个属性, 实现监听多个属性
我们使用间接属性来举例
- 定义一个
Child
类, 它有4个属性:birthday, year, month, day
:
///生日
@property (nonatomic, copy) NSString *birthday;
///生日的年
@property (nonatomic, assign) NSInteger year;
///生日的月
@property (nonatomic, assign) NSInteger month;
///生日的日
@property (nonatomic, assign) NSInteger day;
-
Child.m
中, 初始化上述属性:
- (instancetype)init{
if (self = [super init]) {
self.birthday = @"2000-01-01";
self.year = 2000;
self.month = 1;
self.day = 1;
}
return self;
}
- 定义一个
Worker
类, 它有一个Child
属性:
@property (nonatomic, strong) Child *child;
-
viewController
类中添加一个worker
属性:
@property (nonatomic, strong) Worker *worker;
- 监听worker 的child 中birthday 的改变:
[self.worker addObserver:self forKeyPath:@"child.birthday" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- 添加更改间接属性的事件:
//更改间接属性值事件
- (IBAction)modifyObjectAction:(id)sender {
self.worker.child.birthday = @"2001-12-31";
}
这样在监听方法中, 我们便能得到worker.child.birthday 更改前后的值:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"child.birthday"]) {
NSLog(@“change info = %@", change);
}
}
- 上例中, 对于
Child
来说, 其属性birthday
是由year, month, day
影响的, 即当year, month, day
其一改变时, 关心birthday
的外界也需要收到监听. 这种情况下, 当Child
的year, month 或 day
改变时, 应当告诉birthday
的监听者. - 这里就需要实现 KVO 的这个方法:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingKey
- 在我们重写这个方法, 系统自动补全提示时, 会将Key替换成我们的属性名称. 如此处重写的方法为:
///当kvo当前对象的birthday属性时,如果year,month,day的值发生变化,都会触发这个KVO
+ (NSSet<NSString *> *)keyPathsForValuesAffectingBirthday{
return [NSSet setWithObjects:NSStringFromSelector(@selector(year)),
NSStringFromSelector(@selector(month)),
NSStringFromSelector(@selector(day)),
nil];
}
这样, 只要KVO监听了birthday , 当year, month, day 改变时, 也会触发监听方法.
- 注: 这种操作, 我们在change中得到的还是birthday的值.
监听数组
实际上, 能使用KVO来监听的属性, 必须符合Key-Value Coding, 而数组并不符合.
所以, 直接监听数组属性, 用数组默认的API来操作数组时, 是不会触发监听方法的.
实现:
- 被监听的对象需要实现下面方法
- 且操作数组属性时, 也要使用下面对应的方法:
- objectInMyArrayAtIndex:
- insertObject:inMyArrayAtIndex:
- removeObjectFromMyArrayAtIndex:
- replaceObjectInMyArrayAtIndex:withObject:
同KVO的其它方法一样, 重写这些方法时, 系统也会有补全提示, 而上述中的MyArray会替换成实际的属性名称.
- 依然在上述例子中, 我们为
worker
添加一个cities
属性:
@property (nonatomic, strong) NSMutableArray *cities;
- 在
Worker.m
中初始化:
- (instancetype)init{
if (self = [super init]) {
self.cities = [NSMutableArray array];
}
return self;
}
- 实现KVO数组相关的方法:
- (id)objectInCitiesAtIndex:(NSUInteger)index{
return [self.cities objectAtIndex:index];
}
- (void)insertObject:(NSString *)object inCitiesAtIndex:(NSUInteger)index{
[self.cities insertObject:object atIndex:index];
}
- (void)removeObjectFromCitiesAtIndex:(NSUInteger)index{
[self.cities removeObjectAtIndex:index];
}
- (void)replaceObjectInCitiesAtIndex:(NSUInteger)index withObject:(id)object{
[self.cities replaceObjectAtIndex:index withObject:object];
}
- (void)addCitiesObject:(NSString *)object{
[self.cities addObject:object];
}
- 并在
Worker.h
文件中公开上述方法:
- (id)objectInCitiesAtIndex:(NSUInteger)index;
- (void)insertObject:(NSString *)object inCitiesAtIndex:(NSUInteger)index;
- (void)removeObjectFromCitiesAtIndex:(NSUInteger)index;
- (void)replaceObjectInCitiesAtIndex:(NSUInteger)index withObject:(id)object;
- (void)addCitiesObject:(NSString *)object;
- 在
viewController
中监听:
[self.worker addObserver:self forKeyPath:NSStringFromSelector(@selector(cities)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
- 依次执行下面方法:
[self.worker insertObject:@"nanjing" inCitiesAtIndex:0];
[self.worker insertObject:@"suzhou" inCitiesAtIndex:1];
[self.worker replaceObjectInCitiesAtIndex:1 withObject:@"wuxi"];
[self.worker removeObjectFromCitiesAtIndex:self.worker.cities.count-1];
过滤掉无用信息后, 对应打印结果如下:
//1. [self.worker insertObject:@"nanjing" inCitiesAtIndex:0];
change info = {
kind = 2;
new = (
nanjing
);
}
//2. [self.worker insertObject:@"suzhou" inCitiesAtIndex:1];
change info = {
kind = 2;
new = (
suzhou
);
}
//3. [self.worker replaceObjectInCitiesAtIndex:1 withObject:@"wuxi"];
change info = {
kind = 4;
new = (
wuxi
);
old = (
suzhou
);
}
//4. [self.worker removeObjectFromCitiesAtIndex:self.worker.cities.count-1];
change info = {
kind = 3;
old = (
wuxi
);
}
因为字典change
中存储的是变化的数组元素的值, 而不是整个数组的值, 所以对应步骤解析如下:
- 1.添加.所以只有新值,没有旧值
- 2.同上
- 3.替换.新值替换旧值, 所以既有旧值,也有新值
- 4.删除.只是删除旧值, 没有新值加入,所以只有旧值
- 注:添加元素时,只能
insertObject:AtIndex
, 没有直接addObject:
- 注:添加元素时,只能
关闭系统自动调用KVO, 改为手动调用
在很多情况下, 我们都应该关闭自动调用
, 改为手动调用. 因为每次调用setter, 都会调用监听方法, 即使旧值与新值相同.
如我们要关闭属性name
的自动调用
- 重写触发手动或自动调用的类方法, 并返回NO. 如
+ (BOOL)automaticallyNotifiesObserversOfName{
return NO;
}
或者
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
}
//name之外的属性,还是由系统自动调用
return YES;
}
是的, 正如你所料, 系统是默认返回YES
- 在
name
改变, 需要触发监听方法observeValueForKeyPath: ofObject: change: context:
时, 手动调用-willChangeValueForKey:
及- didChangeValueForKey:
实现
-
把我们的名字数组的
李四
变成张三
, 这样我们就有两个张三
了:
重写
setter
方法:
- (void)setName:(NSString *)name{
if (![_name isEqualToString:name]) {
[self willChangeValueForKey:@"name"];
NSLog(@"22---setter");
_name = [name copy];
[self didChangeValueForKey:@"name"];
}
}
-
打印如下:
利用上述KVO手动调用
的原理, 我们可以监听成员变量. 步骤:
1.添加一个成员变量:
{
int _age;
}
2.监听:
[self addObserver:self forKeyPath:@"_age" options:NSKeyValueObservingOptionNew context:nil];
3.实现监听方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"kvo name = %@", self.name);
}else if ([keyPath isEqualToString:@"_age"]){
NSLog(@"age = %zd", _age);
}
}
4.添加一个修改_age
的事件
///修改age
- (IBAction)modifyAgeAction:(id)sender {
[self willChangeValueForKey:@"_age"];
_age++;
[self didChangeValueForKey:@"_age"];
}
5.打印如下:
context参数
最后我们再来看下addObserver: forKeyPath:options:context:
的context
参数.它是监听的唯一标识,它会被代入监听方法中:observeValueForKeyPath: ofObject: change: context:
通常情况下, 我们不需要 context 参数来区别我们的监听, 但是在下面的小概率事件时:
- 继承
- 父类使用了KVO
就需要用到了.
- 如上述的
viewController
继承自BaseViewController
-
BaseViewController
也使用到了KVO.
此时在viewController
中的方法observeValueForKeyPath: ofObject: change: context:
就覆盖了父类的实现.
解决方法是: - 定义一个唯一的context, 如:
static void *ViewControllerContext = &ViewControllerContext;
- 监听时,传入context:
[self.worker addObserver:self forKeyPath:NSStringFromSelector(@selector(cities)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:ViewControllerContext];
- 在监听方法中,根据context判断:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == ViewControllerContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(cities))]) {
NSLog(@"change info = %@", change);
}
}else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- 如果使用了手动KVO, 也要注意调用super
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}