键值观察
1、KVO实现原理
1、当实例对象 进行KVO观察时候,会利用RuntimeAPI动态生成一个子类,然后将对象的isa指向新生成的子类
2、KVO本质上是监听属性的setter方法,只要被观察对象有成员变量和对应的set方法,就会调用Foundation的_NSSetValueAndNotify函数
这个函数内部会执行 willChangeVlaueForKey函数、
父类的setter方法
和didChangeVlaueForKey的方法(
didChangeVlaueForKey 方法内部会触发监听器的observeValueForKeyPath: ofObject: context:函数
3、子类会重写父类的set、class、dealloc、_isKVOA方法
-
-
重写的
class方法可以指回TCJPerson 原类
-
4、当观察对象移除所有的监听后,会将观察对象的isa指向原来的类
5、当观察对象的监听全部移除后,动态生成的类不会注销,而是留在下次观察时候再使用,避免反复创建中间子类
KVO(Key-Value Observing)是苹果提供的一套事件通知机制,这种机制允许将其他对象的特定属性的更改通知给对象.
iOS开发者可以使用KVO来检测对象属性的变化、快速做出响应,这能够为我们在开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。
要理解KVO,必须先理解KVC,因为键值观察是建立在键值编码的基础上
KVO与NSNotificatioCenter有什么区别呢?
- 相同点
-
1、两者的实现原理都是
观察者模式,都是用于监听 -
2、都能
实现一对多的操作KVO观察中的一对多,意思是通过注册一个KVO观察者,可以监听多个属性的变化
-
- 不同点
-
1、
KVO只能用于监听对象属性的变化,并且属性名都是通过NSString来查找,编译器不会帮你检测对错和补全,纯手敲会比较容易出错 -
2、
NSNotification的发送监听(post)的操作我们可以控制,KVO由系统控制 -
3、
KVO可以记录新旧值变化
-
KVO使用三部曲:
-
注册观察者
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL]; 复制代码 -
实现回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change); } 复制代码 -
移除观察者
[self.person removeObserver:self forKeyPath:@"name"];
通俗的讲,context上下文主要是用于区分不同对象的同名属性,从而在KVO回调方法中可以直接使用context进行区分,可以大大提升性能,以及代码的可读性
//定义context
static void *PersonNameContext = &PersonNameContext;
static void *StudentNameContext = &StudentNameContext;
苹果官方推荐的方式是 —— 在
init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,这是一种比较理想的使用方式
比如有一个下载任务的需求,根据总下载量totalData和当前已下载量writtenData来得到当前下载进度downloadProgress,这个需求就有两种实现方式:
-
分别观察总下载量
totalData和当前已下载量writtenData两个属性,其中一个属性发生变化时计算求值当前下载进度downloadProgress -
实现
keyPathsForValuesAffectingValueForKey方法,并观察downloadProgress属性-
(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方法
KVO观察 可变数组
如题:TCJPerson下有一个可变数组dataArray,现观察之,问点击屏幕是否打印?
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [TCJPerson new];
[self.person addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"dataArray"]) NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person.dataArray addObject:@"1"];
}
复制代码
答:不会 分析:
-
KVO是建立在KVC的基础上的,而可变数组直接添加是不会调用Setter方法 -
可变数组
dataArray没有初始化,直接添加会报错// 初始化可变数组 self.person.dataArray = @[].mutableCopy; // 调用setter方法 [[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"2"];
.动态子类探索
➊首先得明白动态子类观察的是什么?下面观察属性变量nickName和成员变量name来找区别
两个变量同时发生变化,但只有属性变量监听到回调——说明动态子类观察的是setter方法
1.automaticallyNotifiesObserversForKey为YES时注册观察属性会生成动态子类NSKVONotifying_XXX
2.动态子类观察的是setter方法
3.动态子类重写了观察属性的setter方法、dealloc、class、_isKVOA方法 - setter方法用于观察键值 - dealloc方法用于释放时对isa指向进行操作 - class方法用于指回动态子类的父类 - _isKVOA用来标识是否是在观察者状态的一个标志位
4.dealloc之后isa指向元类
5.dealloc之后动态子类不会销毁
四、自定义KVO
新建一个
NSObject+TCJKVO的分类,开放注册观察者方法-(void)cj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(TCJKVOBlock)block;
1.判断当前观察值keypath是否存在/setter方法是否存在
一开始想的是判断属性是否存在,虽然父类的属性不会对子类造成影响,但是分类中的属性虽然没有setter方法,但是会添加到propertiList中去——最终改为去判断setter方法
2. 判断观察属性的automaticallyNotifiesObserversForKey方法返回的布尔值
3.动态生成子类,添加class方法指向原先的类
4.isa重指向——使对象的isa的值指向动态子类
5.保存信息 由于可能会观察多个属性值,所以以属性值-模型的形式一一保存在数组中
②.添加setter方法并回调
往动态子类添加setter方法
③.销毁观察者
往动态子类添加dealloc方法