KVO的定义
- KVO全称为key-value observing即键值观察,是观察者模式中的一种,其提供了一种监听机制,
当对象的指定属性发生变化时
,观察者对象就会收到通知,执行自己的回调;
KVO与NSNotificationCenter的异同点
-
相同点有:
- 两者的实现原理都是观察者模式,用于监听回调;
- 都能实现一对多的操作;
-
不同点有:
- KVO只能用于监听对象属性的变化,而NSNotificationCenter应用很广;
- KVO能够记录对象属性新旧值的变化;
- NSNotification的发送通知(post)的操作我们可以人为的进行控制,KVO由系统控制;
KVO的基础使用
- 创建对象,给对象指定的属性添加KVO监听;
- 更改对象的属性,触发KVO的回调;
- 当对象回收时,移除KVO监听;
- 详情见如下所示:
context参数的作用
- context参数在KVO的监听函数和回调函数中,我们可以在监听函数中设定context的值,然后在回调函数中,根据context的值来
区分KVO监听类别
; - 代码实现如下:
#import "ViewController.h"
#import "YYPerson.h"
@interface ViewController ()
@property(nonatomic,strong)YYPerson *person;
@end
@implementation ViewController
//定义context
static void *PersonNameContext = &PersonNameContext;
static void *PersonNickContext = &PersonNickContext;
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
self.person = [[YYPerson alloc]init];
self.person.name = @"jack";
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:PersonNameContext];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:PersonNickContext];
}
- (void)dealloc{
[self.person removeObserver:self.person forKeyPath:@"name" context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNameContext) {
NSLog(@" name change = %@",change);
}else if (context == PersonNickContext){
NSLog(@" nickName change = %@",change);
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//KVC修改对象的属性
[self.person setValue:@"Tom" forKey:@"name"];
}
@end
KVO使用过程中常见的Crash场景
- 在 iOS异常系列01 -- KVO的异常与防护 文章中详细介绍了使用KVO造成Crash的几种场景,并针对Crash给出了防护方案;
KVO的触发
- 可以通过实现目标对象类的
automaticallyNotifiesObserversForKey
函数,来控制KVO的开启与关闭。
automaticallyNotifiesObserversForKey
函数返回YES,表示开启KVO监听,返回NO,表示关闭KVO监听;当
automaticallyNotifiesObserversForKey
函数返回为NO时,可通过重写对象属性的setter方法
重新开启KVO监听;
KVO可实现注册一个KVO观察者,可以监听多个属性的变化
- 例如YYPerson有三个属性分别为
name
,firstName
,secondName
,现将对firstName
,secondName
的KVO观察合并成一个对name
的观察,通过YYPerson类实现(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
函数来实现;
KVO观察可变数组
- KVO监听可变数组,可变数组通过[self.person.dateArray addObject:@"1"],向数组添加元素,是不会触发KVO通知回调;
- 现做如下修改:
- 向数组中添加元素时,是通过
mutableArrayValueForKey
函数获取可变数组对象,然后再往里面加入元素,才能出发KVO回调; - 其中kind表示键值变化的类型,是一个枚举,主要有以下4种:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//设值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替换
};
- 一般的属性与集合的KVO观察是有区别的,其kind不同,以属性name 和 可变数组为例;
- 属性name的kind一般是设值;
- 可变数组的kind一般是插入;
KVO的底层探索
1> KVO只对对象属性进行监听,触发KVO的回调
- 例如YYPerson定义一个成员变量nickName,一个属性name,然后分别对两者添加KVO监听;
- 只触发属性name的KVO回调,没有触发成员变量nickName的KVO回调;
- KVO只监听对象属性,不会监听对象的成员变量,属性和成员变量的区别在于 属性多一个 setter 方法,而KVO的底层实现恰好依赖于setter方法;
2> 中间类的生成
- 直接上实例代码如下所示:
- person实例对象在添加KVO之前,isa指向YYPerson类;
- person实例对象在添加KVO之后,isa指向NSKVONotifying_YYPerson类;
- person实例对象在添加KVO前后,其isa指针指向发生了变化,也就是说被KVO监听的实例对象其class类型发生了变化;
3> 中间类NSKVONotifying_YYPerson与YYPerson类之间的关系
- 封装一个函数
printClasses
用来获取目标类的子类
实现如下:
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[I]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
- 在person实例对象添加KVO监听前后分别调用,LLDB调试结果如下:
- 可以看出
中间类NSKVONotifying_YYPerson是YYPerson的子类
;
4> 中间类NSKVONotifying_YYPerson的内部成员
- 封装一个函数
printClassAllMethod
用来获取目标类的所有方法
;
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[I];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
- 新建一个YYStudent继承自YYPerson,并重写name的setter方法;
- 调用
[self printClassAllMethod:objc_getClass("NSKVONotifying_YYPerson")]
函数 LLDB打印结果如下:
- 看到NSKVONotifying_YYPerson类中有四个方法,分别为
setName
,class
,dealloc
,_isKVOA
,而YYStudent中没有方法,现在在YYStudent中重写name的setter方法,重新打印结果如下:
与中间类的方法进行的对比说明
只有重写的方法,才会在子类的方法列表中遍历打印出来
,而单纯的继承不重写方法,是不会在子类的方法列表中打印出来的;-
作如下总结:
- NSKVONotifying_YYPerson中间类
重写了父类YYPerson的setName方法
; - NSKVONotifying_YYPerson中间类
重写了基类NSObject的class 、 dealloc 、 _isKVOA方法
; - 其中dealloc是销毁释放;
- _isKVOA判断当前类 是否属于KVO类;
- NSKVONotifying_YYPerson中间类
5> 移除KVO观察者后,person的isa指向是谁,以及中间类NSKVONotifying_YYPerson是否会销毁?
- 在控制器的dealloc方法中移除person对象的KVO监听前后person的isa指向如下:
可以看到person对象移除KVO监听,isa指向更改为YYPerson类;
在上一级控制器touchesBegan方法中调用[self printClasses:[YYPerson class]]函数,LLDB结果如下:
- 可以看出person对象移除KVO监听后,中间类NSKVONotifying_YYPerson依然在内存中没有被销毁;
- 再来看一段测试代码,如下所示:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
[self.view addSubview:self.button];
self.person1 = [[YYPerson alloc]init];
self.person2 = [[YYPerson alloc]init];
NSLog(@"KVO监听之前 %p -- %p",[self.person1 methodForSelector:@selector(setName:)],[self.person2 methodForSelector:@selector(setName:)]);
[self.person1 addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) context:nil];
NSLog(@"KVO监听之后 %p -- %p",[self.person1 methodForSelector:@selector(setName:)],[self.person2 methodForSelector:@selector(setName:)]);
}
- person1添加了KVO监听,person2没有添加KVO监听;
- 在添加KVO监听的前后,打印出被监听属性的set方法,控制台调试如下所示:
- 明显看到person1在添加KVO监听之后,属性name的set方法实现,来自Foundation框架的·
_NSSetObjectValueAndNotify
函数; -
_NSSetObjectValueAndNotify
伪代码实现如下:
_NSSetObjectValueAndNotify(){
[self willChangeValueForKey:name];
//调用父类的set方法
[self setName:name];
[self didChangeValueForKey:name];
}
- (void)didChangeValueForKey:(NSString *)key{
//通知监听者 回调监听方法
}
- 中间类NSKVONotifying_YYPerson会重写setName:方法
- (void)setName:(NSString *)name{
_NSSetObjectValueAndNotify()
}
- 中间类NSKVONotifying_YYPerson会重写class方法
- (Class)class{
return [YYPerson class];
}
- 屏蔽内部实现,隐藏NSKVONotifying_YYPerson类的存在;
- 逻辑原理图:
- 总结:
- 实例对象在注册KVO观察者之后,isa的指向由原有类更改为指向中间类;
- 中间类重写了观察属性的setter方法、class、dealloc、_isKVOA方法;
- 移除KVO监听之后,实例对象isa指向由中间类更改为原有类;
- 中间类从创建后,就一直存在内存中,不会被销毁;
- KVO的实现是基于实例对象的set方法的;