type
status
date
slug
summary
tags
category
icon
password
本文是Objective-C系列的第3篇,主要讲述了KVO的底层实现,以及KVC的使用及KVC中调用流程。
一、概述
KVO
全称Key Value Observing
,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO
的实现机制,只针对属性才会发生作用,一般继承自NSObject
的对象都默认支持KVO
。KVO
和NSNotificationCenter
都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO
是一对一的,而不是一对多的。KVO
对被监听对象无侵入性,不需要修改其内部代码即可实现监听。KVO
可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。集合对象包含NSArray
和NSSet
。二、KVO基本使用
项目代码KVO-01-usage
2.1 注册观察者
2.2 监听回调
2.3 调用
2.3.1 其他调用方式
2.3.2 手动调用
KVO在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现KVO属性的调用,则可以通过KVO提供的方法进行调用。
下面以
age
属性为例:2.3.2.1 禁用自动调用
上面方法也等同于下面方法:
针对每个属性,KVO都会生成一个‘+ (BOOL)automaticallyNotifiesObserversOfXXX’方法,返回是否可以自动调用KVO
假如实现上述方法,我们会发现,此时改变
age
属性的值,无法触发KVO,还需要实现手动调用才能触发KVO。2.3.2.2 手动调用实现
实现了(1)禁用自动调用(2)手动调用实现 两步,
age
属性手动调用就实现了,此时能和自动调用一样,触发KVO。2.4 移除观察者
2.5 Crash
KVO若使用不当,极容易引发Crash。相关试验代码在KVO-02-crash
2.5.1 观察者未实现监听方法
若观察者对象-observeValueForKeyPath:ofObject:change:context:未实现,将会Crash
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<ViewController: 0x7f9943d06710>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
2.5.2 未及时移除观察者
Crash: Thread 1: EXC_BAD_ACCESS (code=1, address=0x105e0fee02c0)
- 假如在当前ViewController中,注册了观察者,点击屏幕,改变被观察对象person1的属性值。
- 点击对应按钮,销毁观察者,此时self.observerPersonChange为nil。
- 再次点击屏幕,此时Crash;
2.5.3 多次移除观察者
Cannot remove an observer <ViewController 0x7fc6dc00c090> for the key path "age" from <BFPerson 0x6000014acd00> because it is not registered as an observer.
2.6 keyPath字符串的弊端
在注册Observe时,传入keyPath为字符串类型,keyPath极容易误写。
优化的方案是:
2.7 属性依赖
三、原理
1. 发现中间对象
为了区分在添加KVO之后,对象以及对应的属性设值方法发生的变化,我们进行了如下测试:
观察方法实现:
- 添加KVO前后,
person1
指向的类对象
和元类对象
,以及setAge:
均发生了变化;
- 添加KVO后,
person1
中的isa
指向了NSKVONotifying_BFPerson类对象;
- 添加KVO之后,
setAge:
的实现调用的是:Foundation 中_NSSetLongLongValueAndNotify
方法;
2. 探索调用流程
重写项目在KVO-03-princlipe。
(1)重写BFPerson的下列方法
(2)调试
我们通过重写方法后,进行打印测试
结合
- 重写打印后的日志:
BFPerson willChangeValueForKey: - begin BFPerson willChangeValueForKey: - end BFPerson setAge: begin BFPerson setAge: end BFPerson didChangeValueForKey: - begin -[ViewController observeValueForKeyPath:ofObject:change:context:]---监听到BFPerson的age属性值改变了 - {kind = 1;new = 29;old = 28;} - age chageBFPerson didChangeValueForKey: - end
- 汇编调用栈
- 中间对象——NSKVONotifying_BFPerson
我们整理出一个完整的方法链:
- self.person1.age = 29;
- Foundation _NSSetLongLongValueAndNotify
- willChangeValueForKey:
- [BFPerson segAge:]
- didChangeValueForKey:
- [ViewController observeValueForKeyPath:ofObject:change:context:]
如下图:
但是这些不足以反应真正完整的KVO实现。
3. 重现KVO(重点)
下面是摘自官方文档给出的原理描述:
Key-Value Observing Implementation DetailsAutomatic key-value observing is implemented using a technique called isa-swizzling.Theisa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.You should never rely on theisa
pointer to determine class membership. Instead, you should use theclass
method to determine the class of an object instance.
我们同时观察添加KVO之后,中间对象的方法列表以及未添加之前的方法列表:
类 | 方法列表(忽略name属性相关方法) |
BFPerson | test, .cxx_destruct, setAge:, age |
NSKVONotify_BFPerson | setAge:, class, dealloc, _isKVOA |
isa
交换技术- 交换之后,调用任何
BFPerson
对象的方法,都会经过NSKVONotify_BFPerson
,但是不同的方法,有不同的处理方式。 - 调用监听的属性设置方法,如
setAge:
,都会先调用NSKVONotify_BFPerson
对应的属性设置方法; - 调用非监听属性设置方法,如
test
,会通过NSKVONotify_BFPerson
的superclass
,找到BFPerson
类对象,再调用其[BFPerson test]
方法 - 交换之后,
isa
指向的并不是该类的真实反映,同样object_getClass
返回的是isa
指向的对象,所以也是不可靠的。比如使用KVO之后,通过object_getClass
得到的是生成的中间对象NSKVONotify_BFPerson
,而不是BFPerson
。 - 要想获得该类真实的对象,需要通过
class
对象方法获取。如通过[self.person1 class]得到的是BFPerson
对象。
- [self.person1 class]得到的仍然是
BFPerson
对象,为什么? NSKVONotify_BFPerson
重写了其class
对象方法,返回的是BFPerson
;
- _isKVOA 返回是否是KVO;
- delloc 做一些清理工作
到此,基本上
NSKVONotifying_BFPerson
类已经成型(相关代码参考项目),结合调用流程,我们绘制出下面对比图。(1)未使用KVO对象
(2)使用KVO——生成中间对象
(3)使用KVO——执行流
四、KVC基本使用
项目源码在:KVC-01-usage
(1)常见的API
其中,有两个方法要注意:
- valueForKey与objectForKey的区别
ㅤ | valueForKey | objectForKey |
无key的处理 | 无该key,crash,NSUndefinedKeyException | 无该key返回nil |
来源 | KVC主要方法 | NSDictionary的方法 |
符号 | 若以 @ 开头,去掉 @ ,用剩下部分作为 key 执行 [super valueForKey:] | key 不是以 @ 符号开头, 两者等同 |
- setValue与setObject的区别
ㅤ | setValue | setObject |
value | value可为nil,当value为nil的时候,会自动调用removeObject:方法 | value是不能为nil |
来源 | KVC的主要方法 | NSMutabledictionary特有的 |
key的参数 | 只能是NSString | setObject: 可任何类型 |
NSKeyValueCoding类别中还有其他的一些方法,例如
(2)集合API
有序集合对应方法如下:
无序集合对应方法如下:
(3)使用场景
a.动态地取值和设值
b.访问和修改私有变量
c.模型转换:Model和字典转换
d.修改一些控件的内部属性
例如设置:UITextField中的placeHolderText
如何获取控件的内部属性?
e.高阶消息传递
当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。
f.KVC中的函数操作集合
- 简单集合运算符
- @avg
- @count
- @max
- @min
- @sum
- 对象运算符
- @distinctUnionOfObjects
- @unionOfObjects
- Array和Set操作符(集合中包含集合的情形)
- @distinctUnionOfArrays
- @unionOfArrays
- @distinctUnionOfSets
五、KVC原理(重点)
项目源码在KVC-02-principle
- setValue:forKey
- valueForKey
六、扩展
1._NSSetLongLongValueAndNotify
参考
链接
示例代码
工具
KVOController Facebook出品的KVO封装库。