美文网首页
第二十二节—KVO(一)

第二十二节—KVO(一)

作者: L_Ares | 来源:发表于2020-11-12 16:15 被阅读0次

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

前两节即然说了KVC,那么接下来一定是基于KVC出现的KVO了。

一、KVO基本简介

  • 英文全称 : Key-Value Observing

  • 中文全称 : 键值观察

  • 作用 : KVO是一种机制,允许在对象的指定属性(看好是属性,没说成员变量)发生更改时通知对象,也可以自己观察自己的指定属性

  • 官方建议 : 应用程序中模型层和控制器层之间的通信。

  • 使用范围 : 但凡是继承了NSObject的类,都可以用。可以监听简单类型,也可以监听集合类型

  • 经常被拿过来对比的对象 : NSNotificatioCenter

    • 相同点 :
      • 实现原理都是观察者模式。
      • 都是可以进行一对多的通知。
    • 不同点 :
      • KVO监听的是对象的属性。NSNotificatioCenter监听的范围就大了。
      • KVO发送监听的动作是由系统来进行的。NSNotificatioCenter则可以利用postNotification方法进行自己的掌控。
      • KVO可以记录属性的旧有值和新值的变化。s
      • KVO使用完了必须销毁NSNotificatioCenteriOS9以后对已经销毁的监听器不会发送通知了,也不会对已经销毁的被监听对象发送消息,从而不会出现野指针的错误。
  • 学习前提 : 想要了解KVO的原理,必须要先学习好KVC的原理。

二、KVO的基本操作API

这个API就按照一般情况下的使用流程来说。

(1). 注册观察者

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

参数解析 :

  • 首先,谁调用了这个方法,谁就是被观察者。比如 : JDPerson的对象person调用了该方法,那么person就会给自己添加一个观察者,person就是被观察者

  • observer : 官方原话 : 为KVO通知注册的对象。说白了就是观察者
    观察者必须实现observeValueForKeyPath:ofObject:change:context:方法。

  • keyPath : 官方原话 : 要观察的属性的路径(相对接收此消息的对象)。说白了就是被观察者的属性名称或路径。这个值不允许为nil

  • options : 官方原话 : NSKeyValueObservingOptions值的组合,它指定在观察通知中包含什么。

    • 下面说一下NSKeyValueObservingOptions这个枚举都有什么。
      • NSKeyValueObservingOptionNew : 指示更改字典应该提供新的属性值(如果适用)。
      • NSKeyValueObservingOptionOld : 指示更改字典应该包含旧的属性值(如果适用)。
      • NSKeyValueObservingOptionInitial : 在观察者注册方法返回之前,应该立即向观察者发送一个通知。
      • NSKeyValueObservingOptionPrior : 在每次更改之前和之后,都向观察者发送单独的通知,而不是在更改之后发送单个通知。
  • context : 官方原话 : 通过observeValueForKeyPath:ofObject:change:context:方法传递给观察者的任意数据。

(2). 观察回调方法

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

方法解析 :

这个方法是观察者必须要实现的方法,在上面的(1)中的observer已经说了。这是一个回调方法,在被观察者特定属性发生了改变之后,观察者通过这个方法得到通知。

参数解析 :

  • keyPath : 被观察者发生更改的值的路径。

  • object : 被观察者

  • change : 一个字典,描述被观察者的属性值所做的更改。

  • context : 在注册观察者的时候提供的context值。一般拿来判断是哪个被观察者属性发生了改变。

(3). 移除观察者

方法解析 :

KVO观察者或被观察者释放之前,必须移除观察者。

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

参数解析 :

  • 首先,谁注册的观察者,谁就要移除观察者。比如上面是person注册的观察者,那么peron就要在观察者delloc里面调用这个方法。

  • observer : 观察者

  • keyPath : 被观察者的被观察的属性名称或路径。

一个小Tip :

移除观察者是在注册观察者之后要进行的事情,如果没有注册观察者就调用移除方法,则会出现NSRangeException。如果你不知道是否对某个对象的某个属性注册了观察者,可以在你认为可能注册的观察者的delloc中使用try/catch,然后尝试移除。

(4). 关于Context

这里要重点说一下这个Context,很多人都是在注册观察者的时候,直接给这个Context赋值为nil,如果不需要使用的话,赋值nil是没问题的,但是还是尽量写成NULL,因为从上述的API可以看出来,context是函数指针,所以NULL更符合语境。

但是!context在一个观察者观察多个被观察者的时候,如果多个被观察者属性名称或者说属性路径也就是keyPath是相同的时候,会更方便,可以直接利用context的不同,来分别被观察者是谁发生了变化。比如 :

创建一个继承与NSObjectJDPerson类,再创建一个继承与JDPersonJDStudent类。在ViewController中给他们的同一个属性name添加观察者为ViewController

- (void)viewDidLoad {
    
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self jd_kvo_addObserver];
    
}

- (void)jd_kvo_addObserver
{
    self.person = [[JDPerson alloc] init];
    self.student = [[JDStudent alloc] init];
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    
}

  • 不使用context,你需要先判断object是谁,然后再根据观察的属性做事情。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    //如果是JDPerson的对象的name属性
    if ([object isMemberOfClass:[JDPerson class]]) {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"这是对self.person对象的name属性发生变化做事情");
        }
    }
    
    //如果是JDStudent的对象的name属性
    if ([object isMemberOfClass:[JDStudent class]]) {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"这是对self.student对象的name属性发生变化做事情");
        }
    }
}
  • 使用context,你只需要通过context就可以判断被观察者是谁,发生变化的同名属性属于哪个被观察者

先在ViewController也就是观察者的上面添加两个全局的静态函数指针。

#import "ViewController.h"
#import "JDPerson.h"
#import "JDStudent.h"

static void* JDPersonNameContext = &JDPersonNameContext;
static void* JDStudentNameContext = &JDStudentNameContext;

@interface ViewController ()

@property (nonatomic, strong) JDPerson *person;

@property (nonatomic, strong) JDStudent *student;

@end

然后在观察回调方法中的判断就可以变为 :

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    //直接就可以用context进行判断
    if (context == JDPersonNameContext) {
        NSLog(@"这是对self.person对象的name属性发生变化做事情");
    }
    else if (context == JDStudentNameContext) {
        NSLog(@"这是对self.student对象的name属性发生变化做事情");
    }
    else {
        //所有不被识别的context都必须归属到super调用该方法
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

结论 :

context的合理利用,比如用于不同被观察者拥有相同的keyPath,可以提高代码的可读性,减少代码的复杂度,提高性能。

三、KVO通知的规则

1. 兼容

为了确保被观察的特定属性是符合KVO机制的,特定属性必须满足以下内容 :

这里说一下,里面所有说的该类都是指 —— 被观察的特定属性所属的类

  1. 该类必须符合KVC的规定。而且KVO支持与KVC相同的数据类型,包括OC对象以及ScalarStructure Support列表中支持的标量和结构。
  1. 该类会为属性发出KVO中的更改通知。
  1. 存在依赖关系的Keys要适当的注册KVO,因为存在依赖关系,所以影响很多。

2. 自动发送KVO通知

在开始的时候介绍说不可以手动的发送通知,其实说的不是很严谨,我们的确。

在默认情况下,遵循KVO机制的类中都有如下一个方法 :

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

作用 :

  • return YES; :这是默认的情况。如果是这种情况,那么无论何时,只要接收到了对这个key做操作的KVC消息,或者调用了key的兼容KVC机制的可变方法,类就会自动调用以下方法 :

    • -willChangeValueForKey:/-didChangeValueForKey:(简单类型的属性用)
    • -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey:(数组类型的属性用)
    • -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects:(集合类型的属性用)
  • return NO; :就不会自动调用上述方法,类就不会发送KVO通知。

3. 手动发送KVO通知

这个就是+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key在某个被观察的类中,被我们写成了return NO;的情况。

如果想要发送通知就要实现2. 自动发送KVO通知return YES调用的几个方法,也就是 :

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

还有另外四个,分别对应了数组类型和集合类型。

4. 属性依赖

JDPerson中创建属性

@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;

其中downloadProgress下载进度是会根据writtenData写入数据和totalData总数据量来决定的,关系是downloadProgress = writtenData / totalData

那么在给downloadProgress添加了观察者ViewController以后,downloadProgress的主要变化还是要看writtenDatatotalData怎么变。

JDPerson.m中实现downloadProgressset方法 :

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

并在这里实现影响downloadProgress属性的两个属性,这是系统方法 :

+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
    
    return [NSSet setWithObjects:@"totalData", @"writtenData",nil];
}

这样就可以达到downloadProgress添加了观察者之后,数值是随着totalDatawrittenData的变换,按照downloadProgress = writtenData / totalData来进行变化了。

5. 可变数组

这里就要清楚的明白一点,也是一直强调的一点,KVO是基于KVC存在的,所以想要使用KVO观察可变数组,那么可变数组的变化必须是通过KVC形式进行的。

JDPerson类中添加可变数组的属性 :

@property (nonatomic, strong) NSMutableArray *dateArray;

在观察者ViewController中添加对它初始化,并且添加观察 :

self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];

这里我们直接使用ViewControllertouchBegin来让dateArray添加元素,然后利用KVO的观察回调方法observeValueForKeyPath来观察变化,

touchBegin :

    //这里要使用KVC的方法获取dateArray,不能直接使用属性的.方法的setter
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

结果 :

图3.5.0.png

这里可以看到一个kind,这个kind会和上面的简单类型的属性不一样,变成了2,简单类型一般都是1。kindNSKeyValueChange类型的枚举,枚举值如下 :

NSKeyValueChange :

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,     //设置值
    NSKeyValueChangeInsertion = 2,   //插入值
    NSKeyValueChangeRemoval = 3,     //移除值
    NSKeyValueChangeReplacement = 4, //替换值
};

对于可变数组和集合,官方文档都是有很详细的书写的,都要用KVC的设值方式才可以进行观察,更官方的文案在这里

那么到这里,KVO的一个最基本,最简单的使用和思路,应该就比较清楚了。普通的使用应该不会有什么问题了,本节就结束,下一节再探索KVO的一个原理。

相关文章

  • 第二十二节—KVO(一)

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。 前两节即然说了KVC,那么接下来一定是基于KVC出现的...

  • iOS原理篇(一): KVO实现原理

    KVO实现原理 什么是 KVO KVO 基本使用 KVO 的本质 总结 一 、 什么是KVO KVO(Key-Va...

  • OC语法:KVO的底层实现

    一、KVO是什么二、怎么使用KVO三、KVO的底层实现四、KVO常见面试题 一、KVO是什么 KVO全称Key-V...

  • 20.iOS底层学习之KVO 原理

    本篇提纲1、KVO简介;2、KVO的使用;3、KVO的一些细节;4、KVO的底层原理; KVO简介 KVO全称Ke...

  • KVO 解析

    KVO解析(一) —— 基本了解KVO解析(二) —— 一个简单的KVO实现KVO解析(三) —— KVO合规性K...

  • KVO基本使用

    分三部分解释KVO一.KVO基本使用二.KVO原理解析三.自定义实现KVO 一、KVO基本使用 使用KVO,能够非...

  • 可能碰到的iOS笔试面试题(7)--KVO-KVC

    KVC-KVO KVC的底层实现? KVO的底层实现? 什么是KVO和KVC? KVO的缺陷? KVO是一个对象能...

  • 04. KVO使用,原理,本质

    问题 KVO日常使用 KVO原理(KVO本质是什么) 如何手动触发KVO 直接修改成员变量会触发KVO吗 KVO图...

  • KVC、KVO

    KVC、KVO探识(一)KVO和KVO的详细使用 KVC、KVO探识(二)KVC你不知道的东西 KVC、KVO探识...

  • 深入理解KVO

    iOS | KVO | Objective-C KVO的本质是什么,如何手动触发KVO? 1.什么是KVO KVO...

网友评论

      本文标题:第二十二节—KVO(一)

      本文链接:https://www.haomeiwen.com/subject/blwjbktx.html