iOS

iOS底层探索之KVO(一)—KVO简介

2021-08-04  本文已影响0人  俊而不逊

回顾

iOS的面试中除了KVC是经常被问到的,还有KVO也是常问的,那么本篇博客就对KVO进行探索和分析下。

KVO

1. 什么是KVO

KVOObjective-C对观察者设计模式的一种实现。KVO提供一种机制,指定一个被观察对象(例如A类),当对象某个属性(例如A中的字符串name)发生更改时,对象会获得通知,并作出相应处理;【且不需要给被观察的对象添加任何额外代码,就能使用KVO机制】。

一般继承自NSObject的对象都默认支持KVOKVO是响应式编程的代表。

Key-Value Observing Programming Guide

2. KVO的使用

2.1 基本使用

- 注册监听

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

observer:添加的监听者的对象,当监听的属性发生改变时会通知这个对象。
keyPath:监听的属性,不能传nil
options:指明通知发出的时机以及change中的键值。
context:是一个可选的参数,可以传任何数据。

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基本使用

从控制台的打印可以看出,在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。集合中的元素被插入,删除,替换时返回234

- NSKeyValueChange

定义如下:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//普通类型设置
    NSKeyValueChangeInsertion = 2,//集合元素插入
    NSKeyValueChangeRemoval = 3,//集合元素移除
    NSKeyValueChangeReplacement = 4,//集合元素替换
};

- context

其他的见名知意,这个context 上下文,平时开发的时候都是直接写个NULL,那么Ta有什么用呢?我们去苹果的KVO官方文档看看。

Context官方文档解释

从官方文档的解释来看就是:

使用Context上下文,是一种更安全、更可扩展的方法来确保收到的通知是发送给我们的观察者而不是superclass

- 移除观察者

我们平时使用KVO的时候,都会在页面销毁的时候移除观察者,那么看看官方是如何解释的。

移除观察者官方解释

例如: 当第一次进入一个页面的时候,我们注册了观察,然后通过某个触摸事件触发了回调,接着我们退出了页面。
当我们第二次进入这个页面的时候,第一次注册的观察者已经被销毁,但是由于这个被观察的对象是个单例,所以依旧会向其观察的对象发送消息,最终导致内存访问异常,应用崩溃。

结论:当我们的页面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"];

willChangeValueForKeydidChangeValueForKey中间进行赋值,则会开启手动监听模式。

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];
}

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"];
}

两次打印的kind值并不一样,那么他们代表什么呢? 其实前面已经说明过了,就是NSKeyValueChangeKey指明了变更的类型

一般情况下返回的都1。集合中的元素被插入,删除,替换时返回234

那么我们现在就验证一下

NSKeyValueChangeKey验证

从代码验证打印的结果可以看出,集合中的元素被插入,删除,替换时返回234

3.总结

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

上一篇 下一篇

猜你喜欢

热点阅读