1. 基本介绍
KVO
全称Key-Value Observing
(键值观察),是一种机制,允许对象在其他对象的指定属性发生更改时收到通知KVO
基于KVC
基础之上,为了理解KVO
,必须首先了解KVC
KVO
和KVC
的差异:KVC
是键值编码,一种由NSKeyValueCoding
非正式协议启用的机制。在对象创建完成后,可以动态的给对象属性赋值KVO
是键值观察,一种监听机制。当指定的对象的属性被修改后,对象会收到通知。所以,KVO
是基于KVC
的基础上,对属性动态变化的监听
KVO
和NSNotificatioCenter
的差异:相同点:
都是观察者模式,都用于监听
都能实现一对多操作
不同点:
KVO只能用于监听对象属性的变化,
NSNotificatioCenter
可以注册任何你感兴趣的东西KVO
发出消息由系统控制,NSNotificatioCenter
由开发者控制KVO
自动记录新旧值变化,NSNotificatioCenter
只能记录开发者传递的参数
2. API介绍
KVO
键值观察的API
使用,分为三部分:
注册观察者
- 使用方法向被观察对象注册观察者
addObserver:forKeyPath:options:context:
- 使用方法向被观察对象注册观察者
接收变更通知
observeValueForKeyPath:ofObject:change:context:
在观察者内部实现以接受更改通知消息
移除观察者
- 当观察者
removeObserver:forKeyPath:
不再应该接收消息时,应该使用方法取消注册观察者。至少在观察者从内存中释放之后调用这个方法
- 当观察者
2.1 注册观察者
观察对象首先通过发送addObserver:forKeyPath:options:context:
消息向被观察对象注册自己,将自身作为观察者和要观察的属性的关键路径传递。观察者另外指定一个选项参数和一个上下文指针来管理通知的各个方面
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
2.2 接收变更通知
当对象的被观察属性的值发生变化时,观察者会收到一条observeValueForKeyPath:ofObject:change:context:
消息。所有观察者都必须实现这个方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change);
}
}
2.3 移除观察者
通过向被观察对象发送removeObserver:forKeyPath:context:
消息、指定观察对象、键路径和上下文来移除键值观察器
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
3. API使用
3.1 context
参数
addObserver:forKeyPath:options:context:
消息中的上下文指针包含任意数据,这些数据将在相应的更改通知中传回给观察者。可以指定NULL
并完全依赖keyPath
字符串来确定更改通知的来源,但是这种方法可能会导致父类由于不同原因也在观察相同键路径的对象出现问题
更安全、更可扩展的方法,是使用上下文来确保您收到的通知是发送给您的观察者而不是父类的
类中唯一命名的静态变量的地址是一个很好的上下文。在父类或子类中以类似方式选择的上下文不太可能重叠。您可以为整个类选择一个上下文,并依靠通知消息中的关键路径字符串来确定发生了什么变化。或者,您可以为每个观察到的keyPath
创建一个不同的上下文,这完全绕过了字符串比较的需要,从而提高了通知解析的效率
查看addObserver:forKeyPath:options:context:
的定义
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
context
参数类型为void *
,如果参数缺失,应该传入NULL
。如果是id
类型,传入nil
context
传入NULL
,表示不使用context
上下文,使用keyPath
区分通知来源
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if([object isEqual:self.person]){
if([keyPath isEqualToString:@"nick"]){
NSLog(@"nick:%@",change);
return;
}
if([keyPath isEqualToString:@"name"]){
NSLog(@"name:%@",change);
return;
}
}
}
设置context
上下文,区分通知来源
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"nick:%@",change);
return;
}
if (context == PersonNameContext){
NSLog(@"name:%@",change);
return;
}
}
3.2 移除观察者
接收到removeObserver:forKeyPath:context:
消息后,观察对象将不再接收observeValueForKeyPath:ofObject:change:context:
指定keyPath
和对象的任何消息
移除观察者时,请记住以下几点:
如果尚未注册为观察者,将其移除会导致
NSRangeException
。您可以调用一次addObserver:forKeyPath:options:context:
,或者此方案在应用程序中不可行,可以将removeObserver:forKeyPath:context:
调用放置在try/catch
块中以处理潜在的异常移除时观察者不会自动将其自身移除。被观察的对象继续发送通知,而忽略了观察者的状态。但是,发送到已释放对象的更改通知与任何其他消息一样,会触发内存访问异常。因此,您要确保观察者在从内存中消失之前将自己移除
该协议没有提供询问对象是观察者还是被观察者的方法。构建您的代码以避免与发布相关的错误。一个典型的模式是在观察者初始化期间注册为观察者(例如:
init
或viewDidLoad
)并在释放期间取消注册(通常是dealloc
),确保正确配对和有序添加和删除消息,并且观察者在从内存中释放之前被取消注册
所以,如果观察者注册后不移除,被观察的对象会继续发送通知,当发送到已释放的对象上,会触发内存访问异常
案例:
将被观察的对象LGStudent
设置为单例模式
第一次进入页面,注册观察者。退出页面,观察者未移除,对象为单例模式不会释放,ViewController
释放
第二次进入页面,注册观察者。当对象修改,第一次的消息还会发送,并且会发送到已经释放的ViewController
中,导致内存访问异常
3.3 KVO
的自动/手动触发
系统默认为自动触发,使用触发开关的好处,在需要的时候进行监听,不需要关闭即可,比自动触发更方便灵活
3.3.1 自动触发
在被观察对象LGPerson
中,设置automaticallyNotifiesObserversForKey
为YES
,开启KVO
的自动触发
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
3.3.2 手动触发
设置automaticallyNotifiesObserversForKey
为NO
,开启KVO
的手动触发
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
手动通知和自动通知并不相互排斥
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
实现手动观察者通知,请willChangeValueForKey:
在更改值之前和didChangeValueForKey:
更改值之后调用
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
3.4 一对多关系
一对多关系,通过注册一个KVO
观察者,可以监听多个属性的变化
例如:完成一个下载进度的需求,下载进度 = 当前下载数currentData / 总下载数totalData
,所以currentData
和totalData
任意值的改变,都会影响到下载进度
分别观察totalData
和currentData
两个属性,当其中一个属性的值发生变化,计算当前下载进度downloadProgress
在被观察对象LGPerson
中,实现keyPathsForValuesAffectingValueForKey:
方法,合并currentData
和totalData
属性的观察
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
实现downloadProgress
属性的getter
方法
- (NSString *)downloadProgress{
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
KVO
代码
self.person.writtenData = 10;
self.person.totalData = 100;
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.writtenData += 10;
self.person.totalData += 1;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"downloadProgress" context:NULL];
}
3.5 监听可变数组
使用KVO
监听可变数组
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
if(self.person.dateArray.count == 0){
[self.person.dateArray addObject:@"1"];
}
else{
[self.person.dateArray removeObjectAtIndex:0];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
代码运行后,发现监听并不生效,点击屏幕后,数组正常添加、移除元素,但接收不到KVO
的回调通知
问题的原因,KVO
基于KVC
基础之上,在KVC
的文档中,说明了对集合对象访问定义的三种不同的代理方法:
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
- 它们返回一个行为类似于
NSMutableArray
对象的代理对象
- 它们返回一个行为类似于
mutableSetValueForKey:
和mutableSetValueForKeyPath:
- 它们返回一个行为类似于
NSMutableSet
对象的代理对象
- 它们返回一个行为类似于
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
- 它们返回一个行为类似于
NSMutableOrderedSet
对象的代理对象
- 它们返回一个行为类似于
修改代码如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
if(self.person.dateArray.count == 0){
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
else{
[[self.person mutableArrayValueForKey:@"dateArray"] removeObjectAtIndex:0];
}
}
-------------------------
//输出以下内容:
{
indexes = "<_NSCachedIndexSet: 0x280fc4720>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
{
indexes = "<_NSCachedIndexSet: 0x280fc4720>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 3;
}
kind
表示键值变化的类型,执行addObject
时,kind
打印值为2
。执行removeObjectAtIndex
时,kind
打印值为3
找到kind
的定义:
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
NSKeyValueChangeSetting
:赋值NSKeyValueChangeInsertion
:插入NSKeyValueChangeRemoval
:移除NSKeyValueChangeReplacement
:替换
4. 实现细节
自动键值观察是使用称为
isa-swizzling
的技术实现该
isa
指针,顾名思义,指向对象的类,它保持一个调度表。该调度表主要包含指向类实现的方法的指针,以及其他数据当观察者为对象的属性注册时,被观察对象的
isa
指针被修改,指向中间类而不是真正的类。因此,isa
指针的值不一定反映实例的实际类永远不要依赖
isa
指针来确定类成员身份。相反,应该使用该class
方法来确定对象实例的类
5. 底层原理
5.1 isa
的变化
打印person
实例对象,在添加KVO
观察者前后,会发生怎样的变化
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
NSLog(@"添加KVO观察者之前:%s", object_getClassName(self.person));
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"添加KVO观察者之后:%s", object_getClassName(self.person));
}
-------------------------
//输出以下内容:
添加KVO观察者之前:LGPerson
添加KVO观察者之后:NSKVONotifying_LGPerson
- 同样
person
实例对象,添加KVO
观察者前后,所属的类对象发生改变
5.2 NSKVONotifying_x
的创建时机
验证NSKVONotifying_LGPerson
类是原本就存在,还是添加KVO
临时生成的
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
NSLog(@"添加KVO观察者之前:%s, %@", object_getClassName(self.person), objc_getClass("NSKVONotifying_LGPerson"));
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"添加KVO观察者之后:%s, %@", object_getClassName(self.person), objc_getClass("NSKVONotifying_LGPerson"));
}
-------------------------
//输出以下内容:
添加KVO观察者之前:LGPerson, (null)
添加KVO观察者之后:NSKVONotifying_LGPerson, NSKVONotifying_LGPerson
- 系统将对象添加
KVO
时,临时生成NSKVONotifying_LGPerson
类,并将实例对象的isa
指向该类
5.3 NSKVONotifying_x
和原始类的关系
验证LGPerson
和NSKVONotifying_LGPerson
的关系
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"NSKVONotifying_LGPerson的Superclass:%@",class_getSuperclass(objc_getClass("NSKVONotifying_LGPerson")));
}
-------------------------
//输出以下内容:
NSKVONotifying_LGPerson的Superclass:LGPerson
NSKVONotifying_LGPerson
是LGPerson
类的子类,即:派生类
5.4 NSKVONotifying_x
中的方法
遍历NSKVONotifying_LGPerson
类中的所有方法
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
unsigned int intCount;
Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LGPerson"), &intCount);
for (unsigned int intIndex=0; intIndex<intCount; intIndex++) {
Method method = methodList[intIndex];
NSLog(@"SEL:%@,IMP:%p",NSStringFromSelector(method_getName(method)), method_getImplementation(method));
}
}
-------------------------
//输出以下内容:
SEL:setNickName:,IMP:0x18a5d8520
SEL:class,IMP:0x18a5d6fd4
SEL:dealloc,IMP:0x18a5d6d58
SEL:_isKVOA,IMP:0x18a5d6d50
NSKVONotifying_LGPerson
重写了父类的setNickName
方法- 重写了
NSObject
类的class
、dealloc
、_isKVOA
方法
5.5 重写class
方法的目的
目的:隐藏KVO
生成的中间类
调用中间类的class
方法,返回的还是原始类对象的地址
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
Class cls = self.person.class;
NSLog(@"添加KVO观察者之前:%s, %p, %@", object_getClassName(self.person), &cls, cls);
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
cls = self.person.class;
NSLog(@"添加KVO观察者之后:%s, %p, %@", object_getClassName(self.person), &cls, cls);
}
-------------------------
//输出以下内容:
添加KVO观察者之前:LGPerson, 0x16f223538, LGPerson
添加KVO观察者之后:NSKVONotifying_LGPerson, 0x16f223538, LGPerson
使用object_getClassName
可以获取实例对象真正所属的类,但使用中间类重写后的class
方法,获取的还是LGPerson
类,仿佛KVO
所做的一切都不存在
5.6 重写dealloc
方法的目的
目的:移除观察者之后,将实例对象的isa
重新指向原始类对象
调用removeObserver
移除观察者,查看实例对象在调用方法前后的变化
- (void)dealloc{
Class cls = self.person.class;
NSLog(@"remove前:%s, %p, %@", object_getClassName(self.person), &cls, cls);
[self.person removeObserver:self forKeyPath:@"nickName"];
cls = self.person.class;
NSLog(@"remove后:%s, %p, %@", object_getClassName(self.person), &cls, cls);
}
-------------------------
//输出以下内容:
移除观察者之前:NSKVONotifying_LGPerson, 0x16ef1e7a8, LGPerson
移除观察者之后:LGPerson, 0x16ef1e7a8, LGPerson
中间类的class
方法,配合dealloc
方法,成功替换了实例对象的isa
指向,并且对开发者毫无痕迹
5.6.1 NSKVONotifying_x
类的销毁
移除观察者之后,实例对象的isa
指向原始类对象,此时中间类的任务已经完成了,它是否会进行销毁呢?
在KVO
案例的前一个页面,点击屏幕,打印NSKVONotifying_LGPerson
类
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"中间类:%@", objc_getClass("NSKVONotifying_LGPerson"));
}
-------------------------
//输出以下内容:
中间类:0x16bc4d168,(null)
添加KVO观察者之前:LGPerson, 0x16bc4b538, LGPerson
添加KVO观察者之后:NSKVONotifying_LGPerson, 0x16bc4b538, LGPerson
移除观察者之前:NSKVONotifying_LGPerson, 0x16bc4e7a8, LGPerson
移除观察者之后:LGPerson, 0x16bc4e7a8, LGPerson
中间类:0x16bc4d168,NSKVONotifying_LGPerson
移除观察者之后,中间类并没有直接销毁。可能考虑再次添加观察者,可以对其进行重用
5.7 重写_isKVOA
方法的目的
目的:标记是否为KVO
生成的中间类
使用KVC
,打印原始类和中间类的isKVOA
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"添加KVO观察者之前:%s,_isKVOA:%@", object_getClassName(self.person), [self.person valueForKey:@"isKVOA"]);
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"添加KVO观察者之后:%s,_isKVOA:%@", object_getClassName(self.person), [self.person valueForKey:@"isKVOA"]);
}
-------------------------
//输出以下内容:
添加KVO观察者之前:LGPerson,_isKVOA:0
添加KVO观察者之后:NSKVONotifying_LGPerson,_isKVOA:1
5.8 重写set
方法的目的
重写set
方法,中间类负责调用KVO
相关的系统函数,然后调用父类的set
方法,保证原始类中的属性赋值成功。当一切都结束以后,中间类继续调用系统函数,最后调用KVO
的回调通知
5.8.1 KVO
只能监听属性
KVO
为了完成nickName
的监听,创建了中间类并重写了setNickName
方法。那对于成员变量来说,它们不存在set
方法,还能使用KVO
进行监听吗?
在LGPerson
中,定义name
成员变量
#import <Foundation/Foundation.h>
@interface LGPerson : NSObject{
@public NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
使用KVO
对其进行监听
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
unsigned int intCount;
Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LGPerson"), &intCount);
for (unsigned int intIndex=0; intIndex<intCount; intIndex++) {
Method method = methodList[intIndex];
NSLog(@"SEL:%@,IMP:%p",NSStringFromSelector(method_getName(method)), method_getImplementation(method));
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person->name = @"KC";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
}
-------------------------
SEL:class,IMP:0x18a5d6fd4
SEL:dealloc,IMP:0x18a5d6d58
SEL:_isKVOA,IMP:0x18a5d6d50
没有set
方法,点击屏幕也没有收到KVO
的监听回调。所以KVO
只能监听属性,无法监听成员变量
5.8.2 set
方法的调用流程
添加KVO
观察者,修改nickName
属性,LGPerson
类中的nickName
属性也会同步修改。这证明在中间类的setNickName
方法中,调用了LGPerson
的setNickName
方法
在lldb
中设置断点
watchpoint set variable self->_person->_nickName
当nickName
赋值时,进入断点,查看函数调用栈
调用LGPerson
的setNickName
方法之前,调用Foundation
库中三个系统方法:
Foundation
_NSSetObjectValueAndNotify:`Foundation
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]:`Foundation
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]:`
调用LGPerson
的setNickName
方法
成功修改nickName
属性,再次调用Foundation
库中两个系统方法:
Foundation
NSKeyValueDidChange:`Foundation
NSKeyValueNotifyObserver:`
最后调用KVO
的回调通知:observeValueForKeyPath:ofObject:change:context:
总结
基本介绍:
KVO
全称Key-Value Observing
(键值观察),是一种机制,允许对象在其他对象的指定属性发生更改时收到通知KVO
基于KVC
基础之上,为了理解KVO
,必须首先了解KVC
- 官方文档:Key-Value Observing Programming Guide
API
介绍:
- 注册观察者
- 使用方法向被观察对象注册观察者
addObserver:forKeyPath:options:context:
- 使用方法向被观察对象注册观察者
- 接收变更通知
observeValueForKeyPath:ofObject:change:context:
在观察者内部实现以接受更改通知消息
- 移除观察者
- 当观察者
removeObserver:forKeyPath:
不再应该接收消息时,应该使用方法取消注册观察者。至少在观察者从内存中释放之后调用这个方法
- 当观察者
API
使用:
- context参数
- 更安全、更可扩展的方法,是使用上下文来确保您收到的通知是发送给您的观察者而不是父类的
- 移除观察者
- 如果观察者注册后不移除,被观察的对象会继续发送通知,当发送到已释放的对象上,会触发内存访问异常
KVO
的自动/手动触发- 系统默认为自动触发,使用触发开关的好处,在需要的时候进行监听,不需要关闭即可,比自动触发更方便灵活
- 设置
automaticallyNotifiesObserversForKey
为YES
,开启自动触发。反正,开启手动触发
- 一对多关系
- 一对多关系,通过注册一个
KVO
观察者,可以监听多个属性的变化
- 一对多关系,通过注册一个
- 监听可变数组
KVO
基于KVC
基础之上,在KVC
的文档中,说明了对集合对象访问定义的三种不同的代理方法:mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
- 它们返回一个行为类似于
NSMutableArray
对象的代理对象
- 它们返回一个行为类似于
mutableSetValueForKey:
和mutableSetValueForKeyPath:
- 它们返回一个行为类似于
NSMutableSet
对象的代理对象
- 它们返回一个行为类似于
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
- 它们返回一个行为类似于
NSMutableOrderedSet
对象的代理对象
- 它们返回一个行为类似于
实现细节:
自动键值观察是使用称为
isa-swizzling
的技术实现该
isa
指针,顾名思义,指向对象的类,它保持一个调度表。该调度表主要包含指向类实现的方法的指针,以及其他数据当观察者为对象的属性注册时,被观察对象的
isa
指针被修改,指向中间类而不是真正的类。因此,isa
指针的值不一定反映实例的实际类永远不要依赖
isa
指针来确定类成员身份。相反,应该使用该class
方法来确定对象实例的类
底层原理:
添加
KVO
观察者,为实例对象所属类动态创建子类,实例对象isa
指向子类中间类重写
set
、class
、dealloc
、_isKVOA
方法重写
class
方法:隐藏KVO
生成的中间类重写
dealloc
方法:移除观察者之后,将实例对象的isa
重新指向原始类对象重写
_isKVOA
方法:标记是否为KVO
生成的中间类重写
set
方法:中间类负责调用KVO
相关的系统函数,然后调用父类的set
方法,保证原始类中的属性赋值成功。当一切都结束以后,中间类继续调用系统函数,最后调用KVO
的回调通知
KVO
只能监听属性,无法监听成员变量set
方法的调用流程:调用
LGPerson
的setNickName
方法之前,调用Foundation
库中三个系统方法:Foundation
_NSSetObjectValueAndNotify:`Foundation
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]:`Foundation
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]:`
调用
LGPerson
的setNickName
方法成功修改
nickName
属性,再次调用Foundation
库中两个系统方法:Foundation
NSKeyValueDidChange:`Foundation
NSKeyValueNotifyObserver:`
最后调用
KVO
的回调通知:observeValueForKeyPath:ofObject:change:context: