一、基本概念
KVO的全称是Key-value observing(键值观察),它提供了一种机制,允许对象在其他对象的特定属性发生变化时收到通知。
二、基本用法
// MyTestClass.h
@interface MyTestClassBase : NSObject
@end
@protocol ATextProtocol <NSObject>
- (void)notImpFun;
@end
@interface MyTestClass : MyTestClassBase <ATextProtocol>
- (void)printClassObject;
+ (void)printClassClass;
@property (nonatomic, assign) BOOL childProperty;
@end
// MyTestClass.m
#import "MyTestClass.h"
#import <objc/runtime.h>
@implementation MyTestClassBase
@end
@implementation MyTestClass
- (void)printClassObject {
NSLog(@"%@ printClassObject", NSStringFromClass(self.class));
Class cls = self.class;
while (cls != Nil) {
NSLog(@"object =%@ class =%@", self, NSStringFromClass(cls));
cls = class_getSuperclass(cls);
}
}
+ (void)printClassClass {
NSLog(@"MyTestClass printClassClass");
Class cls = self;
while (cls != Nil) {
NSLog(@"class =%@", NSStringFromClass(cls));
cls = class_getSuperclass(cls);
}
}
- (void)setChildProperty:(BOOL)childProperty {
_childProperty = childProperty;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
Class cls = self;
while (cls != Nil) {
NSLog(@"resolveInstanceMethod class=%@ sel=%@", NSStringFromClass(cls), NSStringFromSelector(sel));
cls = class_getSuperclass(cls);
}
return [super resolveInstanceMethod:sel];
}
@end
// ViewController.m
@interface ViewController ()
@property (nonatomic, strong) MyTestClass *child;
@property (nonatomic, strong) UIButton *button;
@property (nonatomic, assign) BOOL tap;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_child = [MyTestClass new];
[_child printClassObject];
[MyTestClass printClassClass];
_button = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 120, 60)];
[_button setTitle:@"添加KVO" forState:UIControlStateNormal];
[_button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[_button addTarget:self action:@selector(tap:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_button];
}
- (void)tap:(id)sender {
[_child addObserver:self forKeyPath:@"childProperty" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
_child.childProperty = YES;
_tap = YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"childProperty"]) {
NSLog(@"%@", change);
}
[_child printClassObject];
[MyTestClass printClassClass];
// [_child notImpFun];
}
- (void)dealloc {
if (_tap) {
[_child removeObserver:self forKeyPath:@"childProperty"];
}
}
2.1 基本用法
// 首先需要通过被观察对象调用addObserver,把观察者添加到被观察对象上
// NSKeyValueObservingOptions 指定了能够获取值的类型
// context是观察者与被观察之之间通信的上下文,其用于复杂场景
[_child addObserver:self forKeyPath:@"childProperty" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 然后观察者对象实现observeValueForKeyPath即可
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
2.2 移除观察者
虽然在iOS9之后KVO就不需要手动移除,但是推荐还是手动移除(如果觉得Apple原生的KVO使用麻烦——确实听麻烦的,可以使用一些三方库,他们一版会自动移除观察者)。
An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
以上是官方的说法,大意是:解除分配时,观察者不会自动删除自己。 被观察的对象继续发送通知,而忽略了观察者的状态。 但是,发送到已释放对象的更改通知与任何其他消息一样,会触发内存访问异常。 因此,您要确保观察者在从内存中消失之前将自己移除。
2.3 如何触发observeValueForKeyPath(这里是初步解释,后续有详细说明)
其实很简单,iOS运行时在背后做了一些手脚。为了更清楚观察具体触发过程,请添加如下代码。
// 在MyTestClass.m中为MyTestClass添加如下代码
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
NSLog(@"automaticallyNotifiesObserversForKey");
return YES; // 这里返回YES,则KVO会自动触发,如果返回NO,则需要手动触发
}
-
在VC的Tap事件中设置断点,并执行p *self.child命令,可以看出child的类型是MyTestClass
添加观察者之前 -
注册观察者——此时需要通过调用automaticallyNotifiesObserversForKey方法以获取是否是自动触发的信息
注册观察者 -
自动修改对象类型——把child由MyTestClass修改为NSKVONotifying_为前缀的NSKVONotifying_MyTestClass类型
自动修改对象类型
官方对添加观察者时候对象类型变化的解释
-
修改被观察者的属性值
属性值被修改 -
通知观察者
通知观察者
2.4 梳理与总结:
- 添加观察者时,系统把对象的类型修改为NSKVONotifying_原类型名,这个新类继承自原类型
- 添加观察者时,系统调用automaticallyNotifiesObserversForKey,询问是否自动触发被观察者
- 修改被观察者的属性
- 调用willChangeValueForKey:
- 调用原来类的setter方法([super setAge:age])
- 调用didChangeValueForKey:
- didChangeValueForKey:内部会调用observer的observerValueForKeyPath:ofObject:change:context:方法
- 观察者获得通知
三、高级用法
3.1 手动触发观察者
[_child willChangeValueForKey:@"childProperty"];
_child.childProperty = YES;
[_child didChangeValueForKey:@"childProperty"];
3.2 观察多个属性
当有时某个属性依赖于其他多个属性时,对当前属性的观察相当于需要观察多个属性时,如何处理?参加以下代码。
// MyTestClass.h,为MyTestClass添加以下属性
@property (nonatomic, assign) NSNumber *mainProperty; // 被观察的属性
@property (nonatomic, assign) NSNumber *subProperty1; // 被mainProperty 依赖
@property (nonatomic, assign) NSNumber *subProperty2; // 被mainProperty 依赖
// MyTestClass.m,添加以下方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"mainProperty"]) {
NSArray *affectingKeys = @[@"subProperty1", @"subProperty2"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSNumber *)mainProperty {
return [NSNumber numberWithInteger:_subProperty1.integerValue + _subProperty2.integerValue];
}
// ViewController.m,观察child的mainProperty
- (void)tap:(id)sender {
[_child addObserver:self
forKeyPath:@"mainProperty"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
_child.subProperty1 = @1;
_child.subProperty2 = @2;
_tap = YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"mainProperty"]) {
NSLog(@"%@", change);
}
[_child printClassObject];
[MyTestClass printClassClass];
// [_child notImpFun];
}
/ * _child.subProperty2 = @1; 的输出结果
2021-12-01 10:35:44.693231+0800 StudyKVO[13682:158801] {
kind = 1;
new = 1;
old = 0;
}
*/
/ * _child.subProperty2 = @2; 的输出结果
2021-12-01 10:35:44.693231+0800 StudyKVO[13682:158801] {
kind = 1;
new = 3;
old = 1;
}
*/
3.3 观察集合类型
// MyTestClass.h
@property (nonatomic, strong) NSMutableArray *dataArray;
// MyTestClass.m
- (NSMutableArray*)dataArray {
if (!_dataArray) {
_dataArray = [NSMutableArray array];
}
return _dataArray;
}
// ViewController.m
- (void)tap:(id)sender {
[_child addObserver:self
forKeyPath:@"dataArray"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
[[self.child mutableArrayValueForKey:@"dataArray"] addObject:@1];
_tap = YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"dataArray"]) {
NSLog(@"%@", change);
}
[_child printClassObject];
[MyTestClass printClassClass];
// [_child notImpFun];
}
四、关于NSKVONotifying_原类型名(简称此类为中间类)
4.1 基本内容
被观察者的对象的isa指针执行了一个运行时自动生成的类NSKVONotifying_原类型名,此类继承自被观察者的原始类型。下面对此类进行分析。
2021-12-01 11:25:59.921673+0800 StudyKVO[16739:209645] cls NSKVONotifying_MyTestClass : fun setMyCustomType: --- imp 0x7fff207a3203
2021-12-01 11:25:59.921824+0800 StudyKVO[16739:209645] cls NSKVONotifying_MyTestClass : fun class --- imp 0x7fff207a1d0d
2021-12-01 11:25:59.921939+0800 StudyKVO[16739:209645] cls NSKVONotifying_MyTestClass : fun dealloc --- imp 0x7fff207a1abd
2021-12-01 11:25:59.922053+0800 StudyKVO[16739:209645] cls NSKVONotifying_MyTestClass : fun _isKVOA --- imp 0x7fff207a1ab5
2021-12-01 11:26:00.373625+0800 StudyKVO[16739:209645] ----------
2021-12-01 11:26:01.282659+0800 StudyKVO[16739:209645] cls MyTestClass : fun printClassObject --- imp 0x102476830
2021-12-01 11:26:01.282837+0800 StudyKVO[16739:209645] cls MyTestClass : fun setChildProperty: --- imp 0x1024769f0
2021-12-01 11:26:01.282958+0800 StudyKVO[16739:209645] cls MyTestClass : fun mainProperty --- imp 0x102476c50
2021-12-01 11:26:01.283111+0800 StudyKVO[16739:209645] cls MyTestClass : fun childProperty --- imp 0x102476cd0
2021-12-01 11:26:01.283262+0800 StudyKVO[16739:209645] cls MyTestClass : fun setMainProperty: --- imp 0x102476d00
2021-12-01 11:26:01.283395+0800 StudyKVO[16739:209645] cls MyTestClass : fun subProperty1 --- imp 0x102476d30
2021-12-01 11:26:01.283551+0800 StudyKVO[16739:209645] cls MyTestClass : fun setSubProperty1: --- imp 0x102476d50
2021-12-01 11:26:01.283651+0800 StudyKVO[16739:209645] cls MyTestClass : fun subProperty2 --- imp 0x102476d80
2021-12-01 11:26:01.283735+0800 StudyKVO[16739:209645] cls MyTestClass : fun setSubProperty2: --- imp 0x102476da0
2021-12-01 11:26:01.283814+0800 StudyKVO[16739:209645] cls MyTestClass : fun myCustomType --- imp 0x102476e10
2021-12-01 11:26:01.283906+0800 StudyKVO[16739:209645] cls MyTestClass : fun setMyCustomType: --- imp 0x102476e30
2021-12-01 11:26:01.284014+0800 StudyKVO[16739:209645] cls MyTestClass : fun .cxx_destruct --- imp 0x102476e70
2021-12-01 11:26:01.284093+0800 StudyKVO[16739:209645] cls MyTestClass : fun dataArray --- imp 0x1024768f0
2021-12-01 11:26:01.290066+0800 StudyKVO[16739:209645] cls MyTestClass : fun setDataArray: --- imp 0x102476dd0
--------
- _isKVOA:是一个辨识码,来判断这个类是不是因为KVO产生的动态子类
- dealloc:判断它是否进行释放
- class:是类的信息
- setMyCustomType:是要变化的属性的setter方法
- 在dealloc中移除观察者后,对象的isa就变回原有类型
4.2 总结分析
- 在添加观察时,runtime会产生一个中间类:
- 中间类继承于原类
- 中间类会重写被观察key的setter方法,
- 对象的isa从指向元类,变成指向中间类
- 当对属性赋值时,对象会根据isa找到中间类对应的setter方法,然后在willChangeValueForKey和didChangeValueForKey方法之间进行赋值,进而触发-(void)observeValueForKeyPath:ofObject:change:context:方法。
- 当在dealloc中移除通知后,isa会重新指向原来的类,相关实例变量的值不变。dealloc后中间类并不会释放,依然在注册类中。
4.3 最后的说明
如果你那第二节中的代码进行测试,就会发现在即使在添加观察之后,在VC的代码中po _child.class依然输出的是MyTestClass,但是在MyTestClass的resolveInstanceMethod方法中,获取的class确是NSKVONotifying_MyTestClass。如果你基于类名MyTestClass,在resolveInstanceMethod进行某些处理,那么可能会忽略此错误。
网友评论