type
status
date
slug
summary
tags
category
icon
password
本文是Objective-C系列的第3篇,主要讲述了KVO的底层实现,以及KVC的使用及KVC中调用流程。

一、概述

KVO全称Key Value Observing,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,只针对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO
KVONSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不是一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVCmutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArrayNSSet

二、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)
  1. 假如在当前ViewController中,注册了观察者,点击屏幕,改变被观察对象person1的属性值。
  1. 点击对应按钮,销毁观察者,此时self.observerPersonChange为nil。
  1. 再次点击屏幕,此时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之后,对象以及对应的属性设值方法发生的变化,我们进行了如下测试:
notion image
notion image
观察方法实现:
notion image
notion image
  • 添加KVO前后,person1指向的类对象元类对象,以及setAge:均发生了变化;
  • 添加KVO后,person1中的isa指向了NSKVONotifying_BFPerson类对象;
  • 添加KVO之后,setAge:的实现调用的是:Foundation 中 _NSSetLongLongValueAndNotify方法;

2. 探索调用流程

重写项目在KVO-03-princlipe

(1)重写BFPerson的下列方法

(2)调试

我们通过重写方法后,进行打印测试
notion image
notion image
结合
  • 重写打印后的日志:
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 chage
BFPerson didChangeValueForKey: - end
 
 
  • 汇编调用栈
notion image
notion image
  • 中间对象——NSKVONotifying_BFPerson
我们整理出一个完整的方法链:
  1. self.person1.age = 29;
  1. Foundation _NSSetLongLongValueAndNotify
    1. willChangeValueForKey:
    2. [BFPerson segAge:]
    3. didChangeValueForKey:
      1. [ViewController observeValueForKeyPath:ofObject:change:context:]
如下图:
notion image
notion image
但是这些不足以反应真正完整的KVO实现。

3. 重现KVO(重点)

下面是摘自官方文档给出的原理描述:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa 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 the isa pointer to determine class membership. Instead, you should use the class 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_BFPersonsuperclass,找到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对象

notion image
notion image

(2)使用KVO——生成中间对象

notion image
notion image

(3)使用KVO——执行流

notion image
notion image

四、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
notion image
notion image

五、KVC原理(重点)

项目源码在KVC-02-principle
  • setValue:forKey
notion image
notion image
  • valueForKey
notion image
notion image

六、扩展

1._NSSetLongLongValueAndNotify

参考

 
链接
  1. Key-Value Coding Programming Guide
  1. Key-Value Observing Programming Guide
  1. KVC 和 KVO
  1. Key-Value Observing
 
示例代码
  1. KVC-01-usage
  1. KVC-02-principle
  1. KVO-01-usage
  1. KVO-02-crash
  1. KVO-03-principle
  1. KVOLearnDemo

工具

KVOController Facebook出品的KVO封装库。