探索KVC和KVO的本质
- 这篇文章主要介绍
KVO
和KVC
, 机器底层是如何实现的 KVO
的全称是Key-Value Observing
,俗称键值监听,可以用于监听某个对象属性值的改变KVO
是使用获取其他对象的特定属性变化的通知机制,控制器层的绑定技术就是严重依赖键值观察获得模型层和控制器层的变化通知的- 对于不依赖控制器层类的应用程序,键值观察提供了一种简化的方法来实现检查器并更新用户界面值
KVC
和KVO
都是基于OC
的动态特性和Runtime
机制的
KVO
添加监听
如下所示, 我们为person
对象添加一个监听
1 | - (void)viewDidLoad { |
上面添加监听的方法
1 | addObserver:forKeyPath:options:context: |
当被监听的对象的属性发生改变时就会调用下面的方法
1 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { |
KVO的本质
这里我们创建两个pweson
对象, 但是只对person1
实行监听
1 | self.person1 = [[Person alloc]init]; |
下面我们可以在touchesBegan
方法中分别添加断点打印两个对象的isa
, 如下
- 从上面可以看出,未添加监听的
pweson2
对象的isa
依然是Person
, 但是添加KVO
监听的person1
的isa
变成了NSKVONotifying_Person
NSKVONotifying_Person
这个类是由Runtime
在运行状态下动态创建的一个类, 是Person
的一个子类- 当我们对
age
属性进行赋值操作的时候, 其实调用的是Person
类的setAge
方法person1
通过isa
找到其对应的类对象Person
类, 并调用Person
类的setAge
方法person2
通过isa
找到其对应的类对象NSKVONotifying_Person
类, 并调用NSKVONotifying_Person
类的setAge
方法- 两个类的
setAge
方法的实现是不一样的, 后面会详解
Person
和NSKVONotifying_Person
对应的类对象如下所示
使用了KVO
监听的对象动态生成的NSKVONotifying_Person
类
实际上NSKVONotifying_Person
类中的setAge:
方法内部是调用了Foundation
的_NSSetIntValueAndNotify
方法, 有兴趣的可以反编译一下Foundation.framwork
的源码, 查看其伪代码, 大致的可以推出内部方法的实现, 代码大致如下
1 | - (void)setAge:(int)age |
- 从上面的代码可以看出
_NSSetIntValueAndNotify
其实重写了willChangeValueForKey
和didChangeValueForKey
两个方法 - 而且监听属性值变化的是在
didChangeValueForKey
方法中实现的 - 下面我们就来验证一下上述代码
首先我们在
Person
类内部重写willChangeValueForKey
和didChangeValueForKey
两个方法, 在运行的过程中分别加断点进行调试, 如下
1 | - (void)setAge:(int)age{ |
然后在如下代码中加断点
1 | // 当监听对象的属性值发生改变时,就会调用 |
在输出结果中可以看到代码的执行顺序, 从下面的代码可以看出监听属性的改变其实是在didChangeValueForKey
方法中实现的
1 | setAge: |
KVC
KVC
全称是Key Value Coding
(键值编码),是一个基于NSKeyValueCoding
非正式协议实现的机制,它可以直接通过key
值对对象的属性进行存取操作,而不需通过调用明确的存取方法- 这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定
KVC
提供了一种间接访问属性方法或成员变量的机制,可以通过字符串来访问对象的的属性方法或成员变量- 相关常见的API有
1 | // 通用的访问方法 |
通用访问方法使用示例
1 | // 使用示例 |
keyPath
方法使用示例
1 | //注意,这里要想使用keypath对adress的属性进行赋值,必须先给myself赋一个Address对象 |
底层原理
setValue:forKey:
0. 我们先创建一个Person
类, 并在Person.h
文件中声明一个age
属性, 如下
1 |
|
下面我们在ViewController.m
里面调用一下看看
1 | - (void)viewDidLoad { |
- 如果在
Person.h
文件中没有声明age
属性,也就是在Person.m
文件中没有默认生成的setAge
和getAge
方法 - 那么调用
setValue
方法对age
存值的时候就会导致程序崩溃, 并会报出setValue:forUndefinedKey:]
的错误 - 如同上图中所示,
setValue:forKey:
的原理实际上就是先按照setAge:
和_setAge:
顺序查找方法, 如果找到了对应方法中的一个, 则代码可以执行成功, 下面我们就一个个验证一下吧
1. 验证setKey
和_setKey
方法, 代码如下
1 |
|
在.m
文件中分别添加一下两个方法, 侧其中一个方法的时候, 可以先注释掉另外一个方法
1 |
|
然后在ViewController.m
调用setValue
方法的时候, 可以看到打印对应的输出, 当上述两个方法同事存在的时候, 则会默认执行setAge
方法
1 | [person setValue:@20 forKey:@"age"]; |
2. 如果没有setKey:
和_setKey:
两个方法, 则会继续查找Person.m
文件中是否有accessInstanceVariablesDirectly
方法, 如果没有程序会奔溃
1 |
|
accessInstanceVariablesDirectly
方法默认是返回YES
的, 如果return NO
, 则程序同样会崩溃, 并抛出NSUnknownKeyException
异常- 在
return YES
的情况下, 会按照顺序查找_key、_isKey、key、isKey
等成员变量, 如果找不到依然会抛出NSUnknownKeyException
异常 - 下面在
Person.h
文件中, 分别声明四个变量
1 |
|
在ViewController.m
中添加如下代码, 执行结果如下所示
1 | - (void)viewDidLoad { |
- 当我们在
Person.h
中声明age、isAge、_age、_isAge
四个变量的时候, 上述代码会默认赋值给_age
变量 - 当我们不声明
_age
属性时, 则会默认赋值给_isAge
属性, 以此类推依次是age
和isAge
变量, 有兴趣的可以亲自测试一番
valueForKey
valueForKey
通过key
进行取值的时候, 取值流程和setValue
类似, 途中也比较清晰, 这里就不在赘述了