KVO
什么是KVO
KVO
全称Key Value Observing
,其是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。观察者模式
由于KVO
的实现机制,只针对属性才会发生作用,一般继承自NSObject
的对象都默认支持KVO
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。集合对象包含NSArray
和NSSet
。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。
Key-Value Observing
可翻译成健值观察,是观察者模式再iOS开发中的具体体现,同时也是OC动态性的具体表现。在开发过程中,如果需要外部动态的获得对象的某个属性变化的时机以及变化前后的值,这时候就可以使用KVO
来完成。显然KVO
也属于信息传递的一种方式。
KVO的基本使用
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@property (nonatomic, strong) Student *student;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.student = [[Student alloc] init];
self.student.age = 10;
NSLog(@"%ld", self.student.age);
[self.student addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.student.age = 20;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了! change = %@ ,context = %@", object, keyPath, change, context);
}
@end
这里注意addObserver:forKeyPath:options:context:
方法中的options
options:回调方法里收到被观察的属性的旧值或新值,枚举类型,系统为我们提供了4个方法
NSKeyValueObservingOptionOld
:change中会包含key变化之前的值oldNSKeyValueObservingOptionNew
:change中会包含key变化之后的值newNSKeyValueObservingOptionInitial
:change中不包含key的值,会在kvo注册时候立即发通知NSKeyValueObservingOptionPrior
:会在值发生改变前发出一次通知,改变后通知依然发出,也就是每个change会有两个通知。值变化之前发送通知的 change 中包含notificationIsPrior = 1; 值发生变化之后的的通知 change 不包含上面提到的notificationIsPrior ,可以跟 willChange 手动通知搭配使用- 我们也可以中间以竖线来进行多种选择
NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
这样change既有new又有old
KVO使用注意事项
但是在使用KVO的时候很容易引起crash,所以需要多多注意:
keyPath
不能为空字符串- 注意在适合的地方
removeObersver
,如果观察实例比被观察实例先释放,这时候改变观察属性,会产生崩溃。 - 没有添加,直接移除观察关系,也会产生崩溃
注意这个移除观察者的方法:[xxx removeObserver:self forKeyPath:@"xxxx"]
;
observer
:观察者keyPath
:被观察对象的属性
手动调用KVO
KVO没法实现对数组元素内部的监听,此时就需要我们手动调用KVO
KVO在属性发生改变时的调用时自动的,如果想要手动控制这个调用时机,或想要自己实现KVO属性的调用,则可以通过KVO提供的方法进行调用。
- 如果想要手动调用或者自己实现KVO需要重写下面的方法。该方法返回YES表示允许系统自动调用KVO,NO表示不允许系统自动调用
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"date"]) {
automatic = NO;//对该key禁用系统自动通知,若要直接禁用该类的KVO则直接返回NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
- 另外我们需要重写setter方法:
- (void)setDate:(NSString *)date {
if (date != _date) {
[self willChangeValueForKey:@"date"];
_date = date;
[self didChangeValueForKey:@"date"];
}
}
不过一般情况下手动出发KVO没有什么必要,这样的话即使在没有注册监听者前调用setter方法为属性赋值的时候都会调用道KVO的响应事件,打印结果如下:
所以我们不采用这种方法,直接在注册监听者之后再对被监听的属性重新赋值的时候在赋值操作前后分别调用willChangeValueForKey
和didChangeValueForKey
,例子如下(以对数组进行监听为例):
Apple.h文件中:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Apple : NSObject
@property (nonatomic, strong) NSMutableArray *arrayTest;
@end
NS_ASSUME_NONNULL_END
Apple.m文件中:
#import "Apple.h"
@implementation Apple
- (void)setDate:(NSString *)date {
_date = date;
NSLog(@"setDate:");
}
//由于数组的旧值不能被observeValueForKeyPath方法的change获取到,所以我们将打印旧值的操作移动到willChangeValueForKey中
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
if ([key isEqual:@"arrayTest"]) {
NSLog(@"old value is: %@", self.arrayTest);
}
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
ViewController.h文件中:
#import <UIKit/UIKit.h>
#import "Apple.h"
@interface ViewController : UIViewController
@property (nonatomic, strong) Apple *apple;
@end
ViewController.m文件中:
#import "ViewController.h"
#import "objc/runtime.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_apple = [[Apple alloc] init];
self.apple.arrayTest = [[NSMutableArray alloc] init];
[self.apple addObserver:self forKeyPath:@"arrayTest" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[self.apple willChangeValueForKey:@"arrayTest"];
[self.apple.arrayTest addObject:@"First!"];
[self.apple didChangeValueForKey:@"arrayTest"];
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//打印监听结果
if ([keyPath isEqual:@"arrayTest"]) {
//NSLog(@"old value is: %@", [change objectForKey:@"old"]);
NSLog(@"new value is: %@", [change objectForKey:@"new"]);
}
}
@end
打印结果如下:
可以发现我们已经监听到数组的旧值和新值了。
KVO本质
KVO是基于runtime机制实现的
在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa
指针指向中间类。并且将class
方法重写,返回原类的class
,例子如下:
前提:我们创建一个继承自NSObject类的Apple类,其中定义了一个名为date的字符串属性,并引入其类的头文件:
ViewController.h文件中:
#import <UIKit/UIKit.h>
#import "Apple.h"
@interface ViewController : UIViewController
//声明apple属性,因为KVO只能监听属性
@property (nonatomic, strong) Apple *apple;
@end
ViewController.m文件中:
#import "ViewController.h"
#import "objc/runtime.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_apple = [[Apple alloc] init];
NSLog(@"类对象 -%@", object_getClass(self.apple));
NSLog(@"方法实现 -%p", [self.apple methodForSelector:@selector(setDate:)]);
NSLog(@"元类对象 -%@", object_getClass(object_getClass(self.apple)));
//开启对apple属性的键值监听
[self.apple addObserver:self forKeyPath:@"date" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
NSLog(@"类对象 -%@", object_getClass(self.apple));
NSLog(@"方法实现 -%p", [self.apple methodForSelector:@selector(setDate:)]);
NSLog(@"元类对象 -%@", object_getClass(object_getClass(self.apple)));
}
@end
打印结果如下:
从打印结果的图中,我们可以清晰地看到:
apple
指向的类对象和元类对象以及对应监听的属性的set
方法都发生了改变- 添加
KVO
后,apple
中的isa
指向了NSKVONotifying_Apple
类对象 - 添加
KVO
后,setDate:
的实现调用的是:Foundation
中的_NSSetObjectValueAndNotify
方法
isa-swizzling(类指针交换):
就是把当前某个实例对象的isa
指针指向一个新建造的中间类,在这个新建造的中间类上面做hook
方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。
下图对于这个类指针交换讲解的就非常生动:
NSKVONotifying_Apple内部实现
- setName:最主要的重写方法,set值时调用通知函数
- class:返回原来类的class
- dealloc
- _isKVOA判断这个类有没有被KVO动态生成子类
- (void)setDate:(NSString *)date {
}
- (Class)class {
return [Apple class];
}
- (void)dealloc {
// 收尾工作
}
- (BOOL)_isKVOA {
return YES;
}
isa
指向中间类之后如何调用方法:
调用监听的属性的设置方法,例如:setDate:
,都会先调用NSKVONotify_Apple
对应的属性设置方法
调用非监听属性的设置方法,如print
方法,就会通过NSKVONotify_Apple的superClass
来找到Apple
类对象,在调用其Apple
类对象中的test
方法
为什么要重写class方法:
如果没有重写class
方法,当该对象调用class
方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class
方法是NSObject
中的方法,如果不重写最终可能会返回NSKVONotifying_Apple
,就会将该类暴露出来
setter的实现不同
截图中我们可以看到set
方法的实现在调用KVO
后变成调用_NSSetObjectValueAndNotify
这样一个C函数
我们不知道其本身是什么样,不过我们可以进行测试:
Apple.h文件中:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Apple : NSObject
@property (nonatomic, copy) NSString *date;
@end
NS_ASSUME_NONNULL_END
Apple.m文件里:
#import "Apple.h"
@implementation Apple
- (void)setDate:(NSString *)date {
_date = date;
NSLog(@"setDate:");
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
ViewController.h文件里:
#import <UIKit/UIKit.h>
#import "Apple.h"
@interface ViewController : UIViewController
@property (nonatomic, strong) Apple *apple;
@end
ViewController.m文件里:
#import "ViewController.h"
#import "objc/runtime.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_apple = [[Apple alloc] init];
self.apple.date = @"7Days!";
[self.apple addObserver:self forKeyPath:@"date" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
self.apple.date = @"25Days!";
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//打印监听结果
if ([keyPath isEqual:@"date"]) {
NSLog(@"old value is: %@", [change objectForKey:@"old"]);
NSLog(@"new value is: %@", [change objectForKey:@"new"]);
}
}
@end
要实现该测试样例必须要注意:
- 监听者必须实现
observeValueForKeyPath:ofObject:change:context:
方法 Apple.m
中实现的那三个方法是测试的关键
该样例的打印结果如下:
我们发现:
- 先调用
willChangeValueForKey
方法 - 接着调用原来的
setDate
方法 - 最后调用
didChangeValueForKey
方法,并且通知监听者属性值已经改变,然后监听者执行observeValueForKeyPath:ofObject:change:context:
处理监听事务。
KVO部分相关问题
- KVO的本质是什么?
- 利用
runtime
的API动态生成一个子类,并让实例对象的isa
指向这个全新的子类 - 当修改实例变量对象的属性时候,在全新子类的set方法中会调用
Foundation
的_NSSetXXXValueAndNotify
函数 willChangeValueForKey
- 调用原来的
setter
didChangeValueForKey
:内部会触发监听器的监听方法
- 手动触发KVO(详见上方讲解)
- 直接修改成员变量会触发KVO嘛?
答案是不会
接着我们总结一下KVO的应用场景:
- 需要接收动态变化的时候
- 例如在AVFounditon中获取AVPlayer的播放进度,播放状态,也需要使用KVO来观察。
#pragma mark - 监听
- (void)currentItemAddObserver {
// 监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态
[self.player.currentItem addObserver:self forKeyPath:@"status" options: (NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil];
// 监控缓冲加载情况属性
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
// 缓冲不足暂停了
[self.player.currentItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
// playbackLikelyToKeepUp
[self.player.currentItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
// rate
[self.player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
}
最后总结一下KVO的实现原理
- 在
addObserver:forKeyPath:options:context:context
调用的时候,会自动生成并注册一个该对象(被观察的对象)对应类的子类,取名NSKVONotify_Class
,并且将该对象的isa
指针指向这个新的中间类。 - 在该子类内部实现4个方法-被观察属性的
set
方法、class
方法、isKVO
、delloc
。 - 最关键的是set方法中,先调用
willChangeValueForKey
,再给成员变量赋值,最后调用didChangeValueForKey
,willChangeValueForKey
和didChangeValueForKey
需要成对出现才能生效,在didChangeValueForKey
中会去调用观察者的observeValueForKeyPath: ofObject:
方法。 - 重写
class
方法,这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass
判断的时候出错。
5.isKVO
方法作为实现KVO
功能的一个标识。 delloc
里面还原isa
指针。
KVC
定义在NSKeyValueCoding.h
中,是一个非正式的协议。KVC
提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量
在NSKeyValueCoding
中提供了KVC
通用的访问方法,分别是getter
方法valueForKey
和setter
方法setValue:forKey
,以及其衍生的keyPath
方法,这两个方法是各个类通用的。并且由KVC
提供默认的实现,我们也可以自己重写对应的方法来改变实现。
基本使用
赋值方法
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
取值方法
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
传参nil
如果对非对象传递一个nil
值,KVC
会调用setNIlValueForKey
方法
我们可以重写这个方法来避免
不重写setNIlValueForKey
时:
_apple = [[Apple alloc] init];
[self.apple setValue:nil forKey:@"flagTest"];
程序崩溃了。
重写setNIlValueForKey
后:
首先在Apple.m文件中重写setNIlValueForKey方法为如下(默认的该方法接收到nil之后会打印日志报错):
- (void) setNilValueForKey:(NSString *)key {
return;
}
//再去执行该代码
_apple = [[Apple alloc] init];
[self.apple setValue:nil forKey:@"flagTest"];
重写后程序没有报错。
处理非对象
setValue
时,如果要赋值的对象是基本类型,需要将值封装成NSNumber
或者NSValue
类型valueForKey
时,返回的是id
类型的对象,基本数据类型也会被封装成NSNumber
或者NSValue
valueForKey
可以自动将值封装成对象,但是setValue:forKey:
却不行。我们必须手动讲值类型转换成NSNumber/NSValue
类型才能进行传递
KVC获取值的过程
setValue:forKey
在KVO里面,我们使用setValue:forKey
这一部分会导致触发KVO监听的过程,KVC触发调用了will和did
先用一张图展示一下setValue:forKey
的过程:
- 程序回先通过
setter
方法对属性进行设置 - 如果没有找到
set
方法,KVC机制会检查+(Bool)accessInstanceVariablesDirectly
(直接访问实例变量)方法有没有返回YES
(默认返回YES
)
- 如果重写方法成了
NO
,调用setValueForUndefinedKey:
(为未定义项设置值)抛出异常 - 返回
YES
就去找成员变量并直接赋值,按照_key,_isKey,key,iskey
的顺序找,没找到就抛出异常
赋值顺序的例子如下:
//接口成员变量定义
@interface Apple : NSObject {
@public
int cnt;
int isCnt;
int _isCnt;
int _cnt;
}
//初始化和为成员变量赋值
_apple = [[Apple alloc] init];
_apple -> cnt = 10;
_apple -> _cnt = 11;
_apple -> _isCnt = 12;
_apple -> isCnt = 13;
[_apple setValue:@15 forKey:@"cnt"];
NSLog(@"%@", [_apple valueForKey:@"cnt"]);
NSLog(@"%d %d %d %d", _apple -> cnt, _apple -> _cnt, _apple -> _isCnt, _apple -> isCnt);
打印结果为:
我们可以看到,最后成员变量_cnt
的值由11变成了15,符合我们刚才说的赋值顺序,就是先找_cnt
去赋值,而不是先去找cnt
赋值。
valueForKey
先上流程图参考:
- 先后顺序搜索
getKey、key 、isKey、_getKey、_key
五个方法,若某一个方法被实现,取到的即是方法返回的值,后面的方法不再运行。如果是BOOL
或者int
等类型,会将其包装成一个NSNumber
对象 - 如果五个方法都没有,还是会访问
accessInstanceVariablesDirectly
方法有没有返回YES(该方法默认返回YES
)
- 如果重写方法成了
NO
,抛异常 - 返回
YES
就去找成员变量并取值,取值顺序为_key、_isKey、key、isKey
NSObject(NSKeyValueCoding)
KVC
的Api都声明在NSObject
的分类NSKeyValueCoding
中,所以如果想使用KVC
务必确认该对象是NSObject
的子类。
其中比较重要的API有:
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
key&keyPath
的区别,如果某个属性是一个对象,需要设置该属性的某个属性的时候,就可以使用keyPath
,一步到位来设置或者获取属性,关于这些的详细使用详见上方的讲解。
KVC的使用场景
crash
防护,可以自定义valueForUndefinedKey:
从而实现crash
到控制台打印的友好处理方式json
转model
(这个在接受网络请求来的数据的时候非常好用)KVO
的实现- 访问和修改私有变量(
KVC
的本质是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量。同样如果不想让外界的类使用KVC
的方法访问本类的成员变量,可以将accessInstanceVariablesDirectly
属性设置为NO
) - 修改一些控件的内部属性(很多UI控件都是由内部UI控件组合而成的,但是Apple(苹果官方)没有提供访问这些控件的API,这样我们就无法正常地访问和修改这些空间的样式。而
KVC
在大多数情况下可以解决这个问题)
另外我们在除了利用KVC 动态地对单取值和设值之外,还可以进行多值操作:
KVC
可以根据给定的一组key
,获取到一组value
,并且以字典的形式返回,获取到字典后可以通过key
从字典中获取到value
:
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
同样,也可以通过KVC进行批量赋值。在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含key、value的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给相应对象的属性赋值(其实这里有点像jsonModel):
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
NSDictionary *dic = @{@"name" : @"book", @"age" : @"66", @"sex" : @"male"};
//这个类里面定义了一些属性,这些属性名和传进来的字典的key一致且属性名数量大于等于字典中的key
StudentModel *model = [[StudentModel alloc] init];
[model setValuesForKeysWithDictionary:dic];
NSLog(@"%@",model);
NSDictionary *modelDic = [model dictionaryWithValuesForKeys:@[@"name", @"age", @"studentSex"]];
NSLog(@"modelDic : %@", modelDic);
如果model
属性和dic
不匹配,可以重写方法 -(void)setValue:(id)value forUndefinedKey:(NSString *)key
:
//比如我们需要赋值的属性是studentSex,而传进来的字典的相关对应的key为sex,我们可以在该方法里面将key为sex的value赋值给studentSex属性进行补救
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if([key isEqualToString:@"sex"]) {
self.studentSex = (NSString *)value;
}
}