OC底层原理18 - KVO
简介
KVO
,全称为Key-Value observing
,中文名为键值观察
,KVO是一种机制,它允许将其他对象的指定属性的更改通知给对象
。
在Key-Value Observing Programming Guide官方文档中,又这么一句话:理解KVO之前,必须先理解KVC
(即KVO是基于KVC基础之上)。
KVC是键值编码
,在对象创建完成后,可以动态
的给对象属性赋值,而KVO是键值观察
,提供了一种监听机制,当指定的对象的属性被修改
后,则对象会收到通知
,所以可以看出KVO是基于KVC的基础上对属性动态变化的监听。
KVO与NSNotificatioCenter的区别
- 相同点
- 两者的设计模式都使用
观察者模式
- 都是用于
对象的监听
- 都能实现
一对多
的操作
- 两者的设计模式都使用
- 不同点
- KVO只能用于监听
对象属性的变化
,并且属性名都是通过NSString来查找
,编译器不会帮你检测对错和补全,纯手敲会比较容易出错 - NSNotification的发送监听(post)的操作我们可以控制,KVO由系统控制
- KVO可以
记录新旧值变化
- KVO只能用于监听
KVO 使用
基本操作
- 注册观察者,
addObserver:forKeyPath:options:context
-
observer
参数:注册KVO通知的对象,即观察者;观察者必须实现key-value观察方法:observeValueForKeyPath:ofObject:change:context:
。 -
keyPath
参数:被观察对象的键值路径
,这个值不允许为nil
。 -
options
参数,这是一个NSKeyValueObservingOptions
枚举类型。-
NSKeyValueObservingOptionNew
:当告知观察者对象发生变化时,提供新的
属性值 -
NSKeyValueObservingOptionOld
:当告知观察者对象发生变化时,提供旧的
属性值 -
NSKeyValueObservingOptionInitial
:通知应该立即发送给观察者
,在观察者注册方法甚至返回之前 -
NSKeyValueObservingOptionPrior
:在每次更改之前
和之后
分别向观察者发送通知,而不是在更改之后发送单个通知。这与-willChangeValueForKey:
被触发的时间是相对应的。在每次修改属性时,实际上是会发送两条通知。
-
-
context
参数:任意的额外数据,我们可以将这些数据作为上下文数据并传递至observeValueForKeyPath:ofObject:change:context
方法中。这个参数的意义在于区分同一对象监听同一属性(从属于同一对象)的多个不同的监听。
-
- 实现KVO回调,
observeValueForKeyPath:ofObject:change:context
-
keyPath
参数:被观察对象的属性的键值路径
,这个值不允许为nil
。 -
object
参数:被观察对象。 -
change
参数:这是一个字典,它包含了属性被修改的一些信息。这个字典中包含的值会根据我们在添加观察者时设置的options参数
的不同而有所不同。change中的内容可通过以下key进行获取。官方提供了以下五种Key。-
NSKeyValueChangeKindKey
,提供了发生变化类型的信息。- 返回
NSKeyValueChangeSetting
,表示观察对象被设置了一个新值
。 - 返回
NSKeyValueChangeInsertion
,表示一对多关系
的对象中有值被插入
。 - 返回
NSKeyValueChangeRemoval
,表示一对多关系
的对象中有值被移除
。 - 返回
NSKeyValueChangeReplacement
,表示一对多关系
的对象中有值被替换
。
- 返回
-
NSKeyValueChangeNewKey
:观察对象被设置的新值,需要将options
设置成NSKeyValueObservingOptionNew
。 -
NSKeyValueChangeOldKey
:观察对象被设置新值之前的旧值,需要将options
设置成NSKeyValueObservingOptionOld
。 -
NSKeyValueChangeIndexesKey
:当一对多关系
的对象中有值被插入、移除或替换
,则通过该获取插入、移除或替换
的索引
。 -
NSKeyValueChangeNotificationIsPriorKey
:当options
设置成NSKeyValueObservingOptionPrior
选项时,可以使用NSKeyValueChangeNotificationIsPriorKey
来获取到通知是否是预先发送
的,如果是,获取到的值总是@(YES)
。
-
-
context
参数:这个值即是添加观察者时提供的上下文信息。
-
- 移除观察者,
removeObserver:forKeyPath:context
、removeObserver:forKeyPath
-
observer
参数:注册KVO通知的对象,即观察者。 -
keyPath
参数:被观察对象的键值路径
,这个值不允许为nil
。 -
context
参数:这个值即是添加观察者时提供的上下文信息。
-
移除观察者注意点:
- 如果还没有注册成观察者,就要求被移除会导致
NSRangeException
异常。正常情况,一次removeObserver:forKeyPath:context:
对应于addObserver:forKeyPath:options:context:
。或者在removeObserver:forKeyPath:context:
中增加try/catch
块处理异常。 - 当对象被释放时,观察者不会
自动移除
。被观察对象继续发送通知,发送给已释放对象的更改通知会触发内存访问异常
。因此,可以确保观察者在从内存中消失之前删除自己。 - KVO协议并不提供接口获取当前对象是否是观察者或被观察者。在构建代码时为了避免相关错误,一个典型方法是在
对象初始化
的时候注册成观察者
(例如在init或viewDidLoad
中),在对象释放
的时候移除观察者
(通常在dealloc
中)。
KVO自动触发和手动触发
-
自动触发
当类方法automaticallyNotifiesObserversForKey
返回YES
时,表示自动触发KVO
。当返回NO
时,关闭自动触发KVO
。
NSObject
提供了一个基本的自动键值变化通知
的实现。但不是所有的方法都能自动触发KVO,以下方法能自动触发KVO// Call the accessor method. [account setName:@"Savings"]; //Use setValue:forKey:. [account setValue:@"Savings" forKey:@"name"]; //Use a key path, where 'account' is a kvc-compliant property of 'document'. [document setValue:@"Savings" forKeyPath:@"account.name"]; //Use mutableArrayValueForKey: to retrieve a relationship proxy object. Transaction *newTransaction = <#Create a new transaction for the account#>; NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"]; [transactions addObject:newTransaction];
-
手动触发
当automaticallyNotifiesObserversForKey
返回NO
时,可手动触发KVO
。如下:属性的setter方法中,在改变value之前
调用willChangeValueForKey
,在改变value之后
调用didChangeValueForKey
,- (void)setName:(NSString *)name{ if (name != _name) { [self willChangeValueForKey:@"name"]; _name = name; [self didChangeValueForKey:@"name"]; } }
如果单个操作导致多个属性发生更改,则必须嵌套更改通知。如:
- (void)setBalance:(double)theBalance { [self willChangeValueForKey:@"balance"]; [self willChangeValueForKey:@"itemChanged"]; _balance = theBalance; _itemChanged = _itemChanged+1; [self didChangeValueForKey:@"itemChanged"]; [self didChangeValueForKey:@"balance"]; }
如果
集合类型
的属性,则在手动触发KVO
时,不仅必须指定更改的键,还必须指定更改的类型和涉及的对象的索引。如:- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; }
注册 Dependent Keys
在许多情况下,一个属性的值
依赖于另一个对象中的一个或多个
其他属性的值
。如果一个属性的值
发生了变化
,那么派生属性的值
也应该被标记
为发生了变化
。如何确保为这些依赖属性触发键-值观察通知
,取决于关系的基数。
- 一对一情况
- 当属性发生更改时,
手动触发
派生属性的KVO。 - 重写
keyPathsForValuesAffectingValueForKey:
方法,指明派生属性依赖于哪个属性。 - 实现类方法
keyPathsForValuesAffecting<Key>
,指明派生属性依赖于哪个属性。 - 实现类方法
keyPathsForValuesAffectingValueForKey:
,指明派生属性依赖于哪个属性。
-
一对多情况
keyPathsForValuesAffectingValueForKey:
方法不能支持to-many
的关系。举个例子,比如你有一个 Department 对象,和很多个 Employee 对象。而 Employee 有一个 salary 属性。你可能希望 Department 对象有一个 totalSalary 的属性,依赖于所有的 Employee 的 salary 。
你可以注册 Department 成为所有 Employee 的观察者。当 Employee 被添加或者被移除时,你必须要添加和移除观察者。然后在 observeValueForKeyPath:ofObject:change:context: 方法中,根据改变做出反馈。- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == totalSalaryContext) { [self updateTotalSalary]; } else // deal with other observations and/or invoke super... } - (void)updateTotalSalary { [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]]; } - (void)setTotalSalary:(NSNumber *)newTotalSalary { if (totalSalary != newTotalSalary) { [self willChangeValueForKey:@"totalSalary"]; _totalSalary = newTotalSalary; [self didChangeValueForKey:@"totalSalary"]; } } - (NSNumber *)totalSalary { return _totalSalary; }
KVO中的isa-swizzling
KVO 的实现用了一种叫isa-swizzling
的技术。isa
就是指向类的指针,当一个对象的一个属性注册了观察者后,被观察对象的isa
就指向了一个系统为我们生成的中间类
,而不是我们自己创建的类。在这个类中,系统为我们重写了被观察属性
的setter
方法。
-
中间类
注册KVO
观察者后,观察对象的isa
指向会发生改变,指向了一个NSKVONotifying_xxx
的类。带着这个观点,接下来对代码进行调试分析。
【1】验证注册观察者之后,观察对象的isa
是否会发生改变,是否会指向一个NSKVONotifying_xxx
的类。//注册观察者之前 (lldb) po object_getClassName(self) "PersonSwizzling" //注册观察者之后 (lldb) po object_getClassName(self) "NSKVONotifying_PersonSwizzling" //object_getClassName const char *object_getClassName(id obj) { return class_getName(obj ? obj->getIsa() : nil); }
通过lldb打印信息可以验证,当注册观察者之后,
观察对象的isa
发生了变化。并且指向了NSKVONotifying_PersonSwizzling
的类。【2】PersonSwizzling 类和 NSKVONotifying_PersonSwizzling 类的关系
注册观察者之前//1. 在注册观察者之前,查看当前类的isa以及supperclass //读取对象地址中的内容 (lldb) x/4g self 0x6000038f8410: 0x0000000106a23e80 0x0000000000000000 0x6000038f8420: 0x00007f984be06dc0 0x0000000000000000 //通过对象的isa找到当前实例对象所指向的类 (lldb) p/x 0x0000000106a23e80 & 0x00007ffffffffff8ULL (unsigned long long) $1 = 0x0000000106a23e80 //实例对象指向的类:Person类 (lldb) po $1 PersonSwizzling //读取类地址中的内容 (lldb) x/4g $1 0x106a23e80: 0x0000000106a23ea8 0x00007fff86d54660 0x106a23e90: 0x0000600002ff4740 0x0001801c00000003 //类结构中,第一个参数是类的isa指向,此时指向的是 PersonSwizzling 元类 (lldb) po 0x0000000106a23ea8 PersonSwizzling //类结构中,第二参数是类的父类,此时指向的是 NSObject 类 (lldb) po 0x00007fff86d54660 NSObject
注册观察者之后//2. 在注册观察者之后,查看当前类的isa以及supperclass //读取对象地址中的内容 (lldb) x/4g self 0x6000038f8410: 0x00006000008f81b0 0x0000000000000000 0x6000038f8420: 0x00007f984be06dc0 0x0000000000000000 //通过对象的isa找到当前实例对象所指向的类 (lldb) p/x 0x00006000008f81b0 & 0x00007ffffffffff8ULL (unsigned long long) $5 = 0x00006000008f81b0 //当前实例对象指向的类:NSKVONotifying_PersonSwizzling 类 (lldb) po $5 NSKVONotifying_PersonSwizzling //读取类地址中的内容 (lldb) x/4g $5 0x6000008f81b0: 0x00006000008f8240 0x0000000106a23e80 0x6000008f81c0: 0x00007fff20193d20 0x0000000400000000 //类结构中,第一个参数是类的isa指向,此时指向的是 NSKVONotifying_PersonSwizzling 元类 (lldb) po 0x00006000008f8240 NSKVONotifying_PersonSwizzling //类结构中,第二参数是类的父类,此时指向的是 PersonSwizzling 类 (lldb) po 0x0000000106a23e80 PersonSwizzling
由lldb中可以看出,当注册观察后,原本
实例对象的isa
由指向PersonSwizzling类
修改成指向NSKVONotifying_PersonSwizzling
类,而NSKVONotifying_PersonSwizzling
类的父类
是PersonSwizzling
类。【3】NSKVONotifying_PersonSwizzling 类中有什么方法?
1、通过class_copyMethodList方法获取类的方法列表
2、遍历方法列表,获取每个方法的SEL和IMP- (void)printClassAllMethod:(Class)cls{ unsigned int outCount = 0; Method* methodList = class_copyMethodList(cls, &outCount); for(int i = 0; i<outCount; i++){ Method method = methodList[i]; SEL sel = method_getName(method); IMP imp = method_getImplementation(method); NSLog(@"sel:%@, imp:%p", NSStringFromSelector(sel), imp); } free(methodList); }
通过lldb打印输出可查看到
NSKVONotifying_PersonSwizzling
类中方法的SEL和IMP。2021-03-19 15:47:34.199551+0800 KVODemo[93742:5454700] sel:setName:, imp:0x7fff207bbb57(Foundation`_NSSetObjectValueAndNotify) 2021-03-19 15:47:39.892224+0800 KVODemo[93742:5454700] sel:class, imp:0x7fff207ba662(Foundation`NSKVOClass) 2021-03-19 15:47:45.109027+0800 KVODemo[93742:5454700] sel:dealloc, imp:0x7fff207ba40b(Foundation`NSKVODeallocate) 2021-03-19 15:47:50.819430+0800 KVODemo[93742:5454700] sel:_isKVOA, imp:0x7fff207ba403(Foundation`NSKVOIsAutonotifying)
-
setName
:观察属性的setter方法 -
class
:获取类的方法 -
dealloc
:对象释放的方法 -
_isKVOA
:判断是否自动触发KVO的方法
-
-
中间类总结
- 在注册观察者之后,实例对象的isa会指向一个由系统生成的
NSKVONotifying_PersonSwizzling
类,NSKVONotifying_PersonSwizzling
继承自NSKVONotifying_PersonSwizzling
。 - 在移除观察者之后,实例对象的isa会修改回指向
PersonSwizzling
类 -
NSKVONotifying_PersonSwizzling
类中会重写观察属性的setter
方法、class
方法、dealloc
方法、_isKVOA
方法。 -
NSKVONotifying_PersonSwizzling
类一旦注册到内存中,为了考虑后续的重用问题,中间类将一直存在内存中。
- 在注册观察者之后,实例对象的isa会指向一个由系统生成的
-
自定义KVO
大概了解了KVO的过程,接下来根据了解的KVO的三步骤自定义VKO流程。-
注册观察者:
- 判断对象属性是否有Setter方法,若没有的话,则不允许继续执行KVO
- 动态生成子类(
重点
)
2.1 获取当前类的类名
,拼接当前类名,生成一个新的类名HQKVONotifying_xxx
2.2 根据新的类名,通过NSClassFromString
函数获取一个类结构的指针
2.3 使用objc_allocateClassPair
完善新的类
2.4 通过objc_registerClassPair
函数将类注册至内存
2.5 重写新类的class方法
,如果没有重映射,自定的KVO在注册前后的实例对象person的class就会看到是不一致的,返回的isa更改后的类,即中间类
2.6 重写新类的setter方法
2.7 重写新类的delloc方法
- 修改实例对象的isa,使它指向新生成的类
- 保存当前观察者信息
-
当观察属性被设值时
- 将设值的信息先发送给父类,让父类完全属性值的设置
- 发送通知
hq_observeValueForKeyPath:ofObject:change:context:
,通知观察者当前属性的值发生了变化。
-
移除观察者
- 将关联对象中保存的观察者信息删掉
- 将实例对象的isa重新指回原来的类
-
自定义KVO Demo
本文中的示例,及自定义KVO 请见Demo地址