深入学习KVC和KVO

深入学习KVC和KVO。研究源码NSKeyValueCoding.h和NSKeyValueObserving.h.在Foundation框架中。

KVC

什么是 KVC?

  • KVC 是 Key-Value-Coding 的简称。
  • KVC 是一种可以直接通过字符串的名字 key 来访问类属性的机制,而不是通过调用 setter、getter 方法去访问。
  • 我们可以通过在运行时动态的访问和修改对象的属性。而不是在编译时确定,KVC 是 iOS 开发中的黑魔法之一。

键值编码(key-value coding)是一种间接更改对象状态的方式。通过传入的字符串(key)查找要更改的对象的状态。查找的规则是,先查找以字符串(key)命名的getter和setter方法。如果没有找到对应的方法,再查找key和_key的实例变量。KVC 是字典转模型,模型转字典的神器

KVC的主要方法

KVC 定义了一种按名称访问对象属性的机制,支持这种访问的主要方法是:

  • 设置值
    keyPath包含了key的功能
    key:只能访问当前对象的属性
    keyPath:能利用运算符一层一层往内部访问属性
    对于标量值,会自动进行装箱和拆箱。
1
2
3
4
5
6
7
8
// value的值为OC对象,如果是基本数据类型要包装成NSNumber
- (void)setValue:(id)value forKey:(NSString *)key;

// keyPath键路径,类型为xx.xx
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

// 它的默认实现是抛出异常,可以重写这个函数做错误处理。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
  • 获取值:
1
2
3
4
5
6
- (id)valueForKey:(NSString *)key;

- (id)valueForKeyPath:(NSString *)keyPath;

// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;

KVC的底层实现

当一个对象调用setValue方法时,方法内部会做以下操作:
①检查是否存在相应key的set方法,如果存在,就调用set方法
②如果set方法不存在,就会查找与key相同名称并且带下划线的成员属性,如果有,则直接给成员属性赋值
③如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值
④如果还没找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

KVC 与点语法比较

用 KVC 访问属性和用点语法访问属性的区别:

  • 用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 KVC 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。
  • 相比点语法用 KVC 方式 KVC 的效率会稍低一点,但是灵活,可以在程序运行时决定访问哪些属性。
  • 用 KVC 可以访问对象的私有成员变量。

应用

字典转模型

- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

KVC 总结

键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法直接或通过实例变量访问的机制,非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。

优点:

  • 不需要通过 setter、getter 方法去访问对象的属性,
  • 可以访问私有成员变量的值,可以间接修改私有变量的值。—重点
  • 可以轻松处理集合类(NSArray)。

缺点:

  • 一旦使用KVC你的编译器无法检查出错误,即不会对设置的键、键值路径进行错误检查。
  • 执行效率要低于 setter 和 getter 方法。因为使用 KVC 键值编码,它必须先解析字符串,然后在设置或者访问对象的实例变量。
  • 使用 KVC 会破坏类的封装性。

KVO

KVO 是 Key-Value-Observing 的简称。在Foundation框架中的NSKeyValueObserver.h文件中。

KVO 是一个观察者模式。观察一个对象的属性,注册一个指定的路径,若这个对象的的属性被修改,则 KVO 会自动通知观察者。

更通俗的话来说就是任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.// 注册观察者,实施监听;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];

2.// 回调方法,在这里处理属性发生的变化;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context


3.// 移除观察者;
[self removeObserver:self forKeyPath:@“age"];

KVO是基于runtime机制实现的
Apple 使用了 isa 搅拌技术(isa-swizzling)来实现的 KVO 。当一个观察者注册对象的一个属性 isa 观察对象的指针被修改,指着一个中间类而不是在真正的类。

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的 setter 方法实现真正的通知机制(Person->NSKVONotifying_Person)

当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

原来,这个中间类,继承自原本的那个类。不仅如此,Apple 还重写了 -class 方法,企图欺骗我们这个类没有变,就是原本那个类。更具体的信息,去跑一下 Mike Ash 的那篇文章里的代码就能明白,这里就不再重复。

KVO底层实现

KVO-键值观察机制,原理如下:

1.当给A类添加KVO的时候,runtime动态的生成了一个子类NSKVONotifying_A,让A类的isa指针指向NSKVONotifying_A类,重写class方法,隐藏对象真实类信息

2.重写监听属性的setter方法,在setter方法内部调用了Foundation 的 _NSSetObjectValueAndNotify 函数

3._NSSetObjectValueAndNotify函数内部

a) 首先会调用 willChangeValueForKey

b) 然后给属性赋值

c) 最后调用 didChangeValueForKey

d) 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .

4.重写了dealloc做一些 KVO 内存释放

当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 (isa 指针告诉 Runtime 系统这个对象的类是什么) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例,如下所示:

KVO在调用存取方法之前总是调用 willChangeValueForKey: ,之后总是调用 didChangeValueForkey: 怎么做到的呢?
答案是通过 isa 混写(isa-swizzling)。

第一次对一个对象调用 addObserver:forKeyPath:options:context: 时,框架会创建这个类的新的 KVO 子类,并将被观察对象转换为新子类的对象。在这个 KVO 特殊子类中, Cocoa 创建观察属性的 setter ,大致工作原理如下:

1
2
3
4
5
- (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@"now"];
[super setValue:aDate forKey:@"now"];
[self didChangeValueForKey:@"now"];
}

这种继承和方法注入是在运行时而不是编译时实现的。这就是正确命名如此重要的原因。只有在使用KVC命名约定时,KVO才能做到这一点。

应用

AFNetworking

在AFN中的AFURLSessionManager中使用了KVO

UIProgressView (AFNetworking)

面试题

  1. [※※]addObserver:forKeyPath:options:context:各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?
1
2
3
4
5
6
7
8
// 添加键值观察
/*
1 观察者,负责处理监听事件的对象
2 观察的属性
3 观察的选项
4 上下文
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];

observer中需要实现一下方法:

1
2
3
4
5
6
7
8
// 所有的 kvo 监听到事件,都会调用此方法
/*
1. 观察的属性
2. 观察的对象
3. change 属性变化字典(新/旧)
4. 上下文,与监听的时候传递的一致
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
  1. [※※※]如何手动触发一个value的KVO?

自动触发 KVO 的原理:

键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:didChangevlueForKey:。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, observeValueForKey:ofObject:change:context:会被调用,继而 didChangeValueForKey: 也会被调用。

如果可以手动实现这些调用,就可以实现“手动触发”了。

  1. [※※※]若一个类有实例变量NSString *_foo,调用setValue:forKey:时,可以以foo还是_foo作为key?
    都可以。
  2. [※※※※]KVC的keyPath中的集合运算符如何使用?
  • 必须用在集合对象上或普通对象的集合属性上
  • 简单集合运算符有@avg, @count , @max , @min ,@sum,
  • 格式 @”@sum.age”或 @”集合属性.@max.age
  1. [※※※※]KVC和KVO的keyPath一定是属性么?
    [※※※※※]如何关闭默认的KVO的默认实现,并进入自定义的KVO实现?
    [※※※※※]apple用什么方式实现对一个对象的KVO?

参考:

https://github.com/leejayID/KVC-KVO
Objective-C中的KVC和KVOhttp://yulingtianxia.com/blog/2014/05/12/objective-czhong-de-kvche-kvo/#KVO
如何自己动手实现 KVOhttp://tech.glowing.com/cn/implement-kvo/

文章目录
  1. 1. KVC
    1. 1.0.1. 什么是 KVC?
    2. 1.0.2. KVC的主要方法
    3. 1.0.3. KVC的底层实现
    4. 1.0.4. KVC 与点语法比较
  2. 1.1. 应用
    1. 1.1.1. 字典转模型
    2. 1.1.2. KVC 总结
  • 2. KVO
    1. 2.1. KVO底层实现
  • 3. 应用
    1. 3.0.1. AFNetworking
  • 4. 面试题
  • 本站总访问量 本站访客数人次 ,