记录寄己走过的路

iOS 模式详解—「KVC编码 & KVO监听」

Write in the first【写在最前】


开发过程中,最常见的就是程序的流程取决于你所使用的各种变量和属性的值,根据变量和属性的值确定后面运行的代码。
学好「获取类中属性的变化」这一模块是开发重要部分之一,
目地:为了解决在开发过程中,由需求改变引发的各种蛋疼、繁琐的问题。

本篇文章主要从【KVC & KVO 使用场景相关】学习总结。
在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,以提高文章质量。

KVC & KVO

目录:

  1. 引导
  2. KVC 概论
  3. KVC 常用方法
  4. KVC 对多种数据类型的支持
  5. KVC 实现原理
  6. KVC 基本使用
    1.KVC 简单赋值 & 取值
    2.KVC 访问私有成员变量
    3.KVC 字典转模型
  7. KVO 概论
  8. KVO 使用步骤
  9. KVO 实现原理
  10. KVO 手动发送通知机制

引导


我们有多种方式获取对象的改变。例如,使用委托、通知获取值的改变。如果需要观察多个属性的变化,使用委托或通知会产生大量代码,一个更好用来观察属性变化的方法是使用 键值监听(Key Value Observing,简称KVO)Apple 在自己的软件中大量使用 KVO。使用 KVO 跟踪单个属性或集合(如数组)的变化非常高效,键值观察建立在 键值编码(Key Value Coding,简称KVC) 基础上,也就是任何你想使用 KVO 监听的属性必须符合键值编码。KVO 只需要在观察者方法中添加代码,不需要修改被观察文件内代码,这一点和委托、通知不同。

KVC 和 KVO 提供了一个强大高效的方式来编写代码,学习 KVO 前必须先掌握 KVC,所以 我们按实用开发技巧一点点剖析它。

KVC 概论


KVC(全称 key-value-coding)即键值编码。KVC 的操作方法由NSKeyValueCoding 非正式协议提供,而NSObject(NSKeyValueCoding)就实现了这个协议,也就是说ObjC中几乎所有的对象都支持 KVC 操作,它是一种不通过存取方法(Setter、Getter),而通过属性名称字符串(key)间接访问类属性(实例变量)的机制。

KVC 常用方法


KVC 常用的方法如下:

  • 赋值
1
2
3
4
5
6
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 注解:
setValue:(属性值) forKey:(属性名),(用于简单属性)。
setValue:(属性值) forKeyPath:(属性名),(用于复合属性,进行内部的点语法,层层访问内部的属性; 例如student.name,Student学生模型类中的name属性。
  • 获取值
1
2
3
4
5
6
- (id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
// 注解:
valueForKey:属性名
valueForKeyPath:属性名(用于复合属性)

KVC 对多种数据类型的支持


首先要说的是对于基本数据类型的属性,KVC 的这几个方法会自动装箱和拆箱。其次,KVC 也支持数组和字典等集合数据。这里了解不多,不做过多总结,有兴趣可参考:KVC/KVO原理详解及编程指南

简单示例:KVC 自动类型转换
如:模型类定义的属性是 float money

1
2
3
4
5
6
@property (nonatomic, assign) float money;
// KVC 赋值
[person setValue:@"18" forKeyPath:@"money"];
[person setValue:[NSNumber numberWithInteger:18] forKeyPath:@"money"];
打印输出会自动转换成 float 类型 18.00;

KVC 实现原理


1、[item setValue:@"白开水ln简书" forKey:@"name"];

  • 1.首先去模型中查找有没有 setName,若有,直接调用赋值 [self setName:@"白开水ln简书"]
  • 2.若无,去模型中查找有没有 name 属性,若有,直接访问属性赋值 name = value
  • 3.若无,再去模型中查找有没有 _name 成员变量,若有,直接访问属性赋值 _name = value
  • 4.找不到,就会直接报找不到的错误(valueForUndefinedKey:)。

2、[item setValuesForKeysWithDictionary:dict];

  • 1.遍历字典中所有 key

  • 2.去模型中查找有没有对应的属性。

    1
    2
    3
    4
    [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
    // 2.去模型中查找有没有对应属性 KVC
    [item setValue:value forKey:key];
    }];

KVC 基本使用


KVC 简单赋值 & 取值
1
2
3
4
5
6
LNPerson *person = [[LNPerson alloc] init];
person.dog = [[LNDog alloc] init];
[person.dog setValue:@"阿黄" forKey:@"name"];
[person setValue:@"旺财" forKeyPath:@"dog.name"];
NSLog(@"%@", person.dog.name);

区别:forKey:forKeyPath:
1、forKeyPath 包含了所有 forKey 的功能
2、forKeyPath 进行内部的点语法,层层访问内部的属性
3、注意:key 值一定要在属性中找到,开发中最好使用forKeyPath

KVC 取值

1
2
3
4
5
[person valueForKeyPath:@"name"]
// 取出数组中所有模型的某个属性值
NSArray *allPersons = @[person1, person2, person3];
NSArray *allPersonName = [allPersons valueForKeyPath:@"name"];

KVC 访问私有成员变量
1
2
3
4
5
6
7
@implementation LNPerson
{
int _age;
}
LNPerson *person = [[LNPerson alloc] init];
[person setValue:@"88" forKeyPath:@"age"];

注意:上面的 keyPath 写age 或 _age都可以,KVC 会自动去查找。

KVC 字典转模型

简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NSDictionary *dict = @{
@"name" :@"lurry",
@"money" : @189.88,
//@"development" : @"iOS"-->问题1模型的属性和字典不能一一对应
/*
@"dog" : @{
@"name" : @"wangcai",
@"price" : @8
},
*/ //-->问题2模型中嵌套模型
};
LNPerson *person = [[LNPerson alloc] init];
[person setValuesForKeysWithDictionary:dict];// 等同于下面的代码
// setValuesForKeysWithDictionary: 原理:
// 1.遍历字典中所有key,去模型中查找有没有对应的属性
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
// 2.去模型中查找有没有对应属性 KVC
[item setValue:value forKey:key];
}];
NSLog(@"%@", person);

  • 开发中不建议使用 setValuesForKeysWithDictionary:(把字典中所有值给模型的属性赋值)

  • 问题1:如果服务器多返回几个数据 Key,在模型中系统找不到就会报错。

    1
    reason: '[<LNPerson 0x100304730> valueForUndefinedKey:]: this class is not key value coding-compliant for the key nam.

解决:重写系统方法 setValue:forUndefinedKey:,就不会有报错信息了。
补充:什么时候重写系统方法?
1、想给系统方法添加额外功能 2、不想要系统方法实现

  • 问题2:如果模型中带有模型型,setValuesForKeysWithDictionary 不能用。
    解决:思路,拿到每一个模型属性,去字典中取出对应的值,给模型赋值(提醒:从字典中取值,不一定要全部取出来)。
    建议使用:MJExtension 字典转模型 和 Runtime(根据模型中属性,去字典中取出对应的 value 给模型属性赋值)

  • 模型转成字典

    1
    NSDictionary *dict = [person dictionaryWithValuesForKeys:@[@"name", @"money"]];

KVO 概论


KVOKey-Value-Obersver)即键值监听,利用一个key来找到某个属性并监听其属性值得改变,当该属性发生变化时,会自动的通知观察者,这比通知中心需要post通知来说,简单了许多。其实这也是一种典型的观察者模式。

KVO 使用步骤


  1. 给目标对象的属性添加观察者
  2. 在回调方法中监听属性的变化
  3. 移除观察者

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LNPerson alloc] init];
person.name = @"zs";
/*
- Observer 观察者
- KeyPath 要监听的属性
- options 选项(可选属性值,示例:旧值和新值)
*/
// 1.添加观察者
[self.person addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
person.name = @"ls";// 这里重新赋值
person.name = @"ww";
}
- (void)dealloc{
// 2.移除观察者
[self.person removeObserver:self forKeyPath:@"name"];
}
/**
* 当监听的属性值发生改变
* @param keyPath 要监听的属性
* @param object 要监听的属性所属的对象
* @param change 改变的内容
* @param context 上下文
*/
#pragma mark - KVO
// 3.监听属性的变化
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(LNPerson *)person change:(NSDictionary<NSString *,id> *)change context:(void *)context{
NSLog(@"%@------%@------%@", keyPath, change);
}

KVO 实现原理


当一个类的属性被观察的时候,系统会通过runtime动态的创建一个该类的派生类,并且会在这个类中重写基类被观察的属性的setter方法,而且系统将这个类的isa指针指向了派生类,从而实现了给监听的属性赋值时调用的是派生类的setter方法。重写的setter方法会在调用原setter方法前后,通知观察对象值得改变。此外,派生类还重写了 dealloc 方法来释放资源。

KVO 实现原理

可以看到重写的 setter 方法,给属性赋值的前后分别调用了两个方法。

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

- (void)didChangeValueForKey:(NSString *)key;会调用

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

KVO 手动发送通知机制


默认情况下,KVO 观察到属性变化系统会自动发送通知,但在某些情况下,你可能需要控制何时发送通知。例如:在某些情况下不需要发送通知,或将多个改变合并为一个通知发送。其实我们也可以手动,显式的调用上面两个方法,以使其具有通知机制。
举个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LNPerson alloc] init];
person.name = @"zs";
// 1.添加观察者
[self.person addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
[self willChangeValueForKey:@"name"];
person.name = @"this is a test"; // 直接修改成员变量的值,手动的调用上下两个方法,使其就有通知机制
[self didChangeValueForKey:@"name"];
}
- (void)dealloc{
// 2.移除观察者
[self.person removeObserver:self forKeyPath:@"name"];
}
#pragma mark - KVO
// 3.监听属性的变化
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(LNPerson *)person change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if(object == self && [keyPath isEqualToString:@"name"]) {
NSLog(@"%@------%@------%@", keyPath, change);
} else {
[super observeValueForKeyPath:keyPath ofObject: person change:change context:context];
}}

期待


  • 如果在阅读过程中遇到 error || new ideas,希望你能 messages 我,我会及时改正谢谢。

  • 点击右上角的 喜欢 和 订阅Rss 按钮,可以收藏本仓库,并在 Demo 更新时收到邮件通知。

❄︎ 本文结束    感谢简阅 ^_^. ❄︎

本文标题:iOS 模式详解—「KVC编码 & KVO监听」

文章作者:寄己的路

原始链接:https://sunyonghui.github.io/iOSUI/KVCKVO.html

版权声明: 署名-非商业性使用-禁止演绎 4.0 国际 本博客所有文章除特别声明外均为原创,转载务必请「注明出处链接(可点击) - 作者」,并通过E-mail等方式告知,谢谢合作!