系列文章:OC底层原理系列,OC基础知识系列 上篇文章我们介绍了KVC,此处有传送门。今天这篇文章就来说下和KVC相关联的KVO。
KVO初识
KVO定义
KVO:全称Key-Value observing
,就是我们所熟悉的键值观察
,KVO是一种监听机制,它将观察的指定对象属性更改后通知到观察者
。
在KVO的官方文档中官方文档传送门可以知道,KVO与KVC关系密切,因为KVO的监听属性值变化,这个属性赋值用的是KVC。
KVO与通知
上面说了KVO是监听属性值变化,当属性值变化的时候能够快速响应,告诉观察者,这点和NSNotification有些相似,那么他们两个有什么异同。
- 相同
- 1.两者都是观察者模式,都是监听
- 2.都是一对多
- 不同
- 1.KVO只用于监听属性变化,并且只能通过NSString查找,编译器不会检查NSString的对错。
- 2.NSNotification发送消息实际可以自己控制,而KVO只要属性变化就会自动调用。
KVO使用过程
KVO基本使用流程
KVO使用主要有3个步骤
- 1.注册观察者(context参数是个对象形式,所以要使用NULL,因为NULL是个对象)
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
- 2.实现KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"nick"]) {
NSLog(@"%@",change);
}
}
- 3.移除观察者
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
context的使用
官方文档对context说明如下:
上面的意思就是:context上下文主要用是区分不同对象的同名属性,在KVC的回调方法里可以通过context进行区分。这样的好处就是非常的方便,而且代码可读性高,性能更好更安全。
使用context进行区分的例子
static void *PersonNameContext = &PersonNameContext;
static void *StudentNameContext = &StudentNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNameContext) {
NSLog(@"%@",change);
}else if (context == StudentNameContext) {
NSLog(@"%@",change);
}
}
KVO移除的必要性
官方文档对KVO的移除做了一下说明: 官方文档的说明简单解释:
- 1.
addObserver和removeObserver必须是一一对应关系
,如果删除未注册的观察者,会抛出NSRangeException异常。 - 2.当观察结束,
观察者是不会自己移除自己的
,如果条件触发将忽略观察者状态(是否被销毁)继续发送通知,这样如果观察者被销毁则会出现内存访问异常
。 - 3.协议里没有提供询问对象是观察者还是被观察者的方法,所以为了避免错误,要
将观察者在初始化期间注册为观察者
,并在释放期间注销注册
(通常放在dealloc方法中) 总结一下就是:观察者的注册和移除必须是一一对应关系,如果不移除,会引发错误。 验证下:准备代码
*************LGViewController*************
@implementation LGViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.student = [LGStudent shareInstance];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)dealloc {
}
@end
*************LGDetailViewController*************
@implementation LGDetailViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.student = [LGStudent shareInstance];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)dealloc{
[self.student removeObserver:self forKeyPath:@"name"];
}
@end
运行代码,先进入LGViewController触发监听,而后push到LGDetailViewController页面也触发监听,此时退出LGDetailViewController页面,退出LGViewController页面,然后再次进入LGViewController页面,点击触发监听,就会报如下错误。
原因:由于第一次注册KVO观察者后没有移除,再次进入界面,会导致第二次注册KVO观察者,导致KVO观察的重复注册,而且第一次的通知对象还在内存中,没有进行释放,此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以导致了类似野指针的崩溃
KVO的自动开关以及手动开关
KVO观察的开启有两种方式,自动开启和手动开启
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
return YES;
}
上面的方法就是:是否自动开启KVO观察者模式,YES就是自动开启,NO就是关闭,需要手动去开启。系统默认是开启的
当上面的方法返回NO,也就是KVO监听触发需要手动触发,用下面的方法就能够开启监听
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
KVO监听一对多
KVO观察者中的一对多,意思就是通过注册KVO观察者,对多个属性变化进行监听。
我们以下载进度为例。下载总大小为totalData,当前下载量writtenData。用KVO来监听当前的下载进度downloadProgress。 分别监听totalData和writtenData,这种就不说了,主要说的是通过实现keyPathsForValuesAffectingValueForKey方法,将两个观察对象合并为一个观察对象
// 需要的地方注册通知
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
//3、触发属性值变化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.currentData += 10;
self.person.totalData += 1;
}
//4、移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"currentProcess"];
}
// 实现keyPathsForValuesAffectingValueForKey方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
KVO观察可变数组
KVO的观察是基于KVC的赋值基础之上的,如果直接对可变数组进行添加数组,是不会调用setter方法,也就不会触发KVO,也就是通过[self.person.dateArray addObject:@"1"]方法向数组添加元素,是不会触发kvo通知回调的
。那么如何触发回调呢?上面说了KVO的回调是基于KVC的赋值,所以我们可以去KVC的官方文档中查看关于可变数组的处理
知道如何通过KVC向可变数组里添加元素,下面我们就来监听可变数组
// 注册可变数组KVO观察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
// 触发数组添加数据
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
// 移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
监听回调打印如下:(我们看到了元素被添加进了可变数组) 我们看到打印的有字段kind,这个字段表示的是键值变化类型,是个枚举值,主要有下面4种 不同赋值监听的kind是不同的 我们看到name的赋值kind为赋值,而数组的添加kind为插入
上面将KVO的使用介绍完了,下面就来研究KVO的底层
KVO的底层原理
分析文档
首先我们开下官方文档对于KVO的介绍 文档说的意思:
- 1.KVO是使用一种isa-swizzling的技术实现的
- 2.isa指针,顾名思义是指向维护分配表的对象的类,这个分配表本质上包含了指向类实现的方法和其它数据的指针。
- 3.当观察者为对象的属性注册时,被观察者的isa指针将被修改,指向中间类而不是真正的类。因此isa指针的值不一定反应实例的实际类。
- 4.所以你不应该依赖isa指针来确定成员关系,相反,你应该使用类方法来确定实例对象的类
通过上面的文档我们知道KVO是通过isa-swizzling来实现的,而在对对象属性注册时会生成中间类,并将isa指针指向中间类
。
代码验证
1.KVO对属性观察
在LGPerson中有一个属性nickName和一个成员变量name,给他们分别注册KVO观察,触发属性变化时,都发生些什么
- 准备代码,对name和nickName分别进行监听
- 我们这里通过手势点击来触发KVO
- 在回调里打印chang 我们运行代码,然后对屏幕进行点击,有如下打印:
我们看到kind为1,是赋值类型,也发现只有nickName被监听了,而name没有。
结论:KVO只对属性进行观察,不会对成员变量进行观察,
而成员变量和属性的差别在于属性有setter方法,而成员变量没有
。KVO正是观察setter方法才发通知。
2.中间类
上面的官方文档中说了,如果对对象属性进行注册,就会生成中间类,并将isa指针指向中间类。那么我看看这个中间类是什么
在观察属性前实例对象person的isa指针指向的是LGPerson类 在观察者被注册后,实例对象person的isa指针指向的是NSKVONotifying_LGPerson
结论:文档说的中间类就是NSKVONotifying_类名,KVO注册后就会生成中间类NSKVONotifying_LGPerson(这里是以LGPerson为例)。同时将实例对象的isa指针指向了中间类
。下面我们来研究下NSKVONotifying_LGPerson是不是子类
2.1 判断中间类是否是子类
思路:判断中间类是不是子类,我们可以查看类的所有子类,看看是不是有我们要判断的中间类,如果有就是,如果没有就不是
。准备代码:
在KVO注册前,我们查看打印一次,在KVO注册后,我们再查看打印一次,看看变化。下面我们进行运行
我们看打印,发现KVO注册后,打印的子类是有NSKVONotifying_LGPerson的,说明NSKVONotifying_LGPerson是LGPerson的子类
2.2 中间类都有什么方法
思路:我们看中间类有什么方法,怎么看?肯定是通过methodList了,遍历methodList将存的方法打印出来
。准备代码:
我们在KVO注册后调用printClassAllMethod方法,传入的值:objc_getClass("NSKVONotifying_LGPerson")
。下面运行代码
我们看到打印的结果NSKVONotifying_LGPerson是有4个方法,分别是setNickName、class、dealloc、_isKVOA,那么这些方法是继承还是重写呢?
- 在LGStudent中重写setNickName方法再获取下LGPerson的所有方法
上面的方法说明了
只有子类重写的方法才会在子类的方法列表中打印出来,而继承的不会
。明白这个后我们再看下LGPerson类的所有方法 我们看Person类的方法跟NSKVONotifying_LGPerson的方法进行对比,得出如下结论: - 1.
NSKVONotifying_LGPerson中间类重写了父类LGPerson的setName方法
- 2.
NSKVONotifying_LGPerson中间类重写了NSObject的class、dealloc、_isKVOA方法(其中dealloc是释放方法,_isKVOA是判断当前类是否是KVO)
2.3 dealloc中移除观察者后,isa指向是谁,以及中间类是否会销毁?
我们上面知道KVO注册后,实例对象person的isa就指向NSKVONotifying_LGPerson,下面我们看下在要销毁钱,isa指针是否会变化
我们看到在销毁KVO之前,person的isa指针依然指向的是NSKVONotifying_LGPerson
。我们看下销毁后会不会发生变化
结论:通过上面的打印对比我们可以看出,KVO移除前实例对象person的isa指针始终指向的是NSKVONotifying_LGPerson,但是KVO被移除后,实例对象person的isa指针就会重新指回到LGPerson
。
2.4 KVO移除后,中间类NSKVONotifying_LGPerson是否还存在?
我们在KVO注册页面的上个页面遍历查找LGPerson的所有子类(因为从KVO注册页面退出,KVO就会移除,此时来到的就是注册页面的上个页面,此时如果SKVONotifying_LGPerson销毁了,那子类就不存在SKVONotifying_LGPerson
)。准备代码:
我们用点击屏幕来触发请求所有子类的方法。,运行代码
我们发现此时还有NSKVONotifying_LGPerson,说明KVO销毁,NSKVONotifying_LGPerson并没有从内存中移除。原因:考虑到重用问题(KVO再次注册),中间一旦注册到内存中去,就不会销毁
。
总结
综上所述,关于中间类,有如下说明:
实例对象在注册KVO观察者之后,isa指针由原有类更改为指向中间类
中间类重写了观察属性的setter方法、class、dealloc、_isKVOA方法
dealloc方法中,移除KVO观察者之后,实例对象isa指向由中间类更改为原有类
中间类从创建后,就一直存在内存中,不会被销毁
写到最后
这篇文章主要讲了KVO的定义,用法,以及监听的中间过程。又分析了中间类有关问题。对KVO有了一个较为全面的认识,下篇文章我们会来自定义一个KVO来实现属性的监听。