回顾
在iOS
的面试中除了KVC
是经常被问到的,还有KVO
也是常问的,那么本篇博客就对KVO
进行探索和分析下。
1. 什么是KVO
KVO
是 Objective-C
对观察者设计模式的一种实现。KVO
提供一种机制,指定一个被观察对象(例如A类),当对象某个属性(例如A中的字符串name
)发生更改时,对象会获得通知,并作出相应处理;【且不需要给被观察的对象添加任何额外代码,就能使用KVO
机制】。
一般继承自NSObject
的对象都默认支持KVO
。KVO
是响应式编程的代表。
2. KVO的使用
2.1 基本使用
- 注册监听
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
observer
:添加的监听者的对象,当监听的属性发生改变时会通知这个对象。
keyPath
:监听的属性,不能传nil
。
options
:指明通知发出的时机以及change
中的键值。
context
:是一个可选的参数,可以传任何数据。
- options
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,//更改前的值
NSKeyValueObservingOptionOld = 0x02,//更改后的值
NSKeyValueObservingOptionInitial = 0x04,//观察最初的值(在注册观察服务时会调用一次触发方法)
NSKeyValueObservingOptionPrior = 0x08 //分别在值修改前后触发方法(即一次修改有两次触发)
};
- 接收监听的属性发生改变的通知
observeValueForKeyPath
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- 移除监听removeObserver
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
2.2 举例
下面就简单的举个🌰,监听下
student
属性name
的变化。
从控制台的打印可以看出,在
KVO
的监听回调observeValueForKeyPath
方法里面,监听到了name
属性的变化,并打印出来变化信息。
- NSKeyValueChangeKey
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
NSKeyValueChangeKey
指明了变更的类型,一般情况下返回的都1
。集合中的元素被插入,删除,替换时返回2
、3
、4
- NSKeyValueChange
定义如下:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//普通类型设置
NSKeyValueChangeInsertion = 2,//集合元素插入
NSKeyValueChangeRemoval = 3,//集合元素移除
NSKeyValueChangeReplacement = 4,//集合元素替换
};
-
NSKeyValueObservingOptionNew
:指明change
字典中应该包含改变后的新值。 -
NSKeyValueObservingOptionOld
:指明change
字典中应该包含改变前的旧值。 -
NSKeyValueObservingOptionInitial
:注册后立马调用一次,这种通知只会发送一次。可以做一些一次性的工作。当同时指定new/old/initial
的情况时,initial
通知只包含new
值。(实际上还是old
值,因为是注册后立马调用,所以实际上对它来说是新值。任何情况下initial
都不会包含old
) -
NSKeyValueObservingOptionPrior
:修改前后触发,会调用两次。修改前触发会包含notificationIsPrior
字段。当同时指定new/old
时,修改前会包含old
,修改后会包含new
和old
。(一般的通知发出时机都是在属性改变后,虽然change
字典中包含了old
和new
,但是通知还是在属性改变后才发出)。 -
0
:直接传递0
,在每次调用的时候都返回包含kind
的change
。可以理解为默认实现。
- context
其他的见名知意,这个context
上下文,平时开发的时候都是直接写个NULL
,那么Ta
有什么用呢?我们去苹果的KVO
官方文档看看。
从官方文档的解释来看就是:
使用
Context
上下文,是一种更安全、更可扩展的方法来确保收到的通知是发送给我们的观察者而不是superclass
。
-
context
会被传递到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。 - 对于多个
keyPath
的观察,需要在observeValueForKeyPath
同时判断object
与keyPath
,可以声明一个静态变量传递给context
用来区分不同的通知提高代码的可读性。 - 如果子类和父类都实现了对同一对象的同一属性的观察,并且父类和子类都可能对其进行设值,那么这个时候就可以利用
context
来进行区分了。
- 移除观察者
我们平时使用KVO的时候,都会在页面销毁的时候移除观察者,那么看看官方是如何解释的。
- 当
deallocated
时,观察者不会自动删除自己。 被观察的对象会继续发送通知,而忽略了观察者的状态。 - 然而,给一个已释放de 对象发送任何其他的通知消息,会触发
内存访问异常
。 - 因此,要确保观察者在从内存中消失之前将它
移除
。
例如
: 当第一次进入一个页面的时候,我们注册了观察,然后通过某个触摸事件触发了回调,接着我们退出了页面。
当我们第二次进入这个页面的时候,第一次注册的观察者已经被销毁,但是由于这个被观察的对象是个单例,所以依旧会向其观察的对象发送消息,最终导致内存访问异常,应用崩溃。
结论
:当我们的页面dealloc
时,观察者一定要移除
,以防止内存泄漏,出现指针。
2.3 自动/手动开启KVO
- 自动开启KVO
使用KVO
时,默认情况下都是自动监听模式,而当我们想改变成手动监听模式的时候,我们需要在被监听的对象中实现automaticallyNotifiesObserversForKey
方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
//可以根据不同的key值,来区分使用自动还是手动监听
if ([key isEqualToString:@"name"]) {
return YES;
}
return NO;
}
如果直接return NO
则表示全部使用手动监听,这时候触摸屏幕的事件就没有任何响应了,如果想要响应则需要实现下面的方法。
- 手动开启KVO
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
在willChangeValueForKey
和didChangeValueForKey
中间进行赋值,则会开启手动监听模式。
2.4 观察多个因素影响的属性
有时候需要观察的属性,是由多个其他的因素共同影响而变化的。
例如在下载文件的过程,下载进度 = 已下载 / 总数
。如果已下载和总数都是在不断变化的,那么我们该怎么做才能对下载进度进行观察呢?举个🌰例子
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [JPPerson new];
[self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.writtenData += 10;
self.person.totalData += 20;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"ViewController :%@",change);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
}
-
点击屏幕三次,分别打印结果如下
- 除了第一次打印了三条数据,其他都是两次,为什么呢?那么去看看方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSString *)downloadProgress {
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
- 在
keyPathsForValuesAffectingValueForKey
方法中,将和downloadProgress
相关的两个因素totalData
和writtenData
通过setByAddingObjectsFromArray
关联起来, - 那么每次
totalData
或者writtenData
改变时,系统会自动通知我们downloadProgress
改变了。 - 而第一次的打印多打印了一次的原因是,当代码执行到
self.person.writtenData += 10
赋值时,会走- (NSString *)downloadProgress
方法,而此次totalData
为0
时,设置为100
,当代码执行到self.person.totalData += 20;
时,totalData
就改变了两次,就会走两次监听方法,加上self.person.writtenData += 10
赋值时writtenData
的改变,一共就是三次了。
2.5 KVO对可变数组的观察
例如对一个对象里面的数组进行监听:
self.person.dateArray = [NSMutableArray array];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
[self.person.dateArray addObject:@"jay"];
}
在viewDidLoad
中实现这些代码,理论上进入页面后就会观察到并回调,而实际上并没有。于是去看苹果的文档,有了重大发现,如下
In order to understand key-value observing, you must first understand key-value coding
这句话的意思,要想理解KVO
就先要理解KVC
,也就是说KVO
是建立在KVC
上的。
使用
KVO
去观察集合类型
的数据变化,那么就需要使用对应的api
来获取这个集合,这样在你进行设置值的时候,系统就能够通知到你。
- 代码修改之后
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [JPPerson new];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
self.person.dateArray = [NSMutableArray array];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"jay"];
}
- 结果打印出来了,第一次是
dateArray
初始化的打印,第二次是addObjcet
添加数据的打印
两次打印的
kind
值并不一样,那么他们代表什么呢? 其实前面已经说明过了,就是NSKeyValueChangeKey
指明了变更的类型一般情况下返回的都
1
。集合中的元素被插入,删除,替换时返回2
、3
、4
那么我们现在就验证一下
从代码验证打印的结果可以看出,集合中的元素被插入,删除,替换时返回
2
、3
、4
。
3.总结
- 使用
KVO
必须注册观察者。 - 使用
KVO
在dealloc
移除观察者。 -
KVO
自动还是手动开启只要实现+ (BOOL)automaticallyNotifiesObserversForKey:
方法,return NO
表示手动,return YES
表示自动。 - 要想理解KVO就先要理解KVC,也就是说KVO是建立在KVC上的。
更多内容持续更新
🌹 喜欢就点个赞吧👍🌹
🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹