第二十二节—KVO(一)

2020-11-12  本文已影响0人  L_Ares

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

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

一、KVO基本简介

二、KVO的基本操作API

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

(1). 注册观察者

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

参数解析 :

(2). 观察回调方法

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

方法解析 :

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

参数解析 :

(3). 移除观察者

方法解析 :

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

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)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];
    
}

- (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属性发生变化做事情");
        }
    }
}

先在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;

作用 :

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的一个原理。

上一篇 下一篇

猜你喜欢

热点阅读