目录:
参考的博客:
GNUstep KVC/KVO探索(二):KVO的内部实现此博客KVO原理讲解的非常到位
iOS八股文(十九)KVC、KVO
[iOS开发]KVO+KVC
教你一行代码使用 KVO(Facebook 出品 FBKVOController 源码使用及解读)
KVO
什么是KVO
KVO
全称Key Value Observing
,其是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。观察者模式
由于KVO
的实现机制,只针对属性才会发生作用,一般继承自NSObject
的对象都默认支持KVO
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。集合对象包含NSArray
和NSSet
。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。
Key-Value Observing
可翻译成健值观察,是观察者模式再iOS
开发中的具体体现,同时也是Object-C
动态性的具体表现。在开发过程中,如果需要外部动态的获得对象的某个属性变化的时机以及变化前后的值,这时候就可以使用KVO
来完成。显然KVO
也属于信息传递的一种方式。
KVO的基本使用
主要分为三个步骤:
- 通过
addObserver:forKeyPath:options:context:
方法注册观察者observer:
观察者,监听属性变化的对象。该对象必须必须实现observeValueForKeyPath:ofObject:change:context:
方法。keyPath:
要观察的属性名称。要和属性声明的名称一致options:
回调方法里收到被观察的属性的旧值或新值,枚举类型,系统为我们提供了4个方法NSKeyValueObservingOptionOld:change
中会包含key
变化之前的值old
NSKeyValueObservingOptionNew:change
中会包含key
变化之后的值new
NSKeyValueObservingOptionInitial:change
中不包含key
的值,会在kvo
注册时候立即发通知NSKeyValueObservingOptionPrior:
会在值发生改变前发出一次通知,改变后通知依然发出,也就是每个change
会有两个通知。值变化之前发送通知的change
中包含notificationIsPrior = 1;
值发生变化之后的的通知change
不包含上面提到的notificationIsPrior
,可以跟willChange
手动通知搭配使用- 我们也可以中间以竖线来进行多种选择
NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
这样change
既有new
又有old
- 观察对象发生改变,回调方法
observeValueForKeyPath:ofObject:change:context:
keyPath:
被观察对象的属性object:
被观察的对象change:
字典类型,存放相关的值,根据options
传入的枚举来返回新值旧值或者noticationlsPrior = 1
context:
注册观察者时候context
传入的值
- 当观察者不需要监听时,可以调用
removeObserver:forKeyPath:
方法将KVO
移除,我们需要在观察者消失之前进行处理,否则就crash
了
具体的使用样例可以详见我之前的博客:[iOS]-KVO基础
对于对已经注册的KVO
观察者的dealloc
来说还存在一些问题:
也就是说,我们调用addObserver:selector:name:object:
方法创建的观察者就没有必要自己写[xxx removeObserver:self forKeyPath:@"xxxx"];
来删除观察者了。
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机制实现的 ,具体的KVO底层实现可以参考该博客:GNUstep KVC/KVO探索(二):KVO的内部实现
在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的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
判断的时候出错。 isKVO
方法作为实现KVO
功能的一个标识。delloc
里面还原isa
指针。
到这里,我们已经知道了KVO
的原理和其使用以及注意事项,人们对于KVO
可谓是又爱又恨,因为其原理让我们在类之前通信增加了一种方式,但确实容易crash
,针对这个问题,FaceBook
的开发者的第三方库FBKVOController
就巧妙的解决了这一问题,下面我们详细讲解一下FBKVOController
第三方库。
FBKVOController浅析
首先介绍一下FBKVOController
的用法:
- 使用前先利用
cocoa
向项目中下载FBKVOController
第三方库,具体第三方库的下载流程详见:[iOS]-Masonry的使用博客中前面的讲解。 - 在文件中引入下方头文件:
#import "FBKVOController.h"
#import "NSObject+FBKVOController.h"
接下来我们就可以开始正常的使用了
使用例子如下:
ViewController.h文件中:
#import <UIKit/UIKit.h>
#import "Apple.h"
@interface ViewController : UIViewController
@property (nonatomic, strong) Apple *apple;
@end
#import "ViewController.h"
#import "objc/runtime.h"
#import "FBKVOController.h"
#import "NSObject+FBKVOController.h"
@interface ViewController ()
@property (nonatomic, strong) FBKVOController *kvoController;
@end
ViewController.m文件中:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"hello world!");
//创建被观察的类
_apple = [[Apple alloc] init];
//初始化设置_apple的属性值
self.apple.date = @"7Days!";
self.apple.arrayTest = [[NSMutableArray alloc] init];
//方法一:创建 FBKVOController 对象,并被 VC 强引用,否则出了当前作用域,就会被销毁
FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self];
_kvoController = kvoController;
//添加观察
[kvoController observe:self.apple keyPath:@"date" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) { NSLog(@"我的旧名字是:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"我的新名字是:%@", change[NSKeyValueChangeNewKey]);
}];
//方法二:无需主动创建 FBKVOController 对象,self.KVOController 直接懒加载创建FBKVOController 对象
//可以直接对某个对象的多个成员变量执行 KVO
//------真正实现一行代码搞定 KVO------
[self.KVOController observe:self.apple keyPaths:@[@"date", @"arrayTest"] options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
NSString *changedKeyPath = change[FBKVONotificationKeyPathKey];
if ([changedKeyPath isEqualToString:@"date"]) {
NSLog(@"修改了日期!");
} else if ([changedKeyPath isEqualToString:@"arrayTest"]) {
NSLog(@"修改了数组!");
}
NSLog(@"旧值是:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"新值是:%@", change[NSKeyValueChangeNewKey]);
}];
//修改被监听的值
self.apple.date = @"31Days!";
}
@end
Apple.h文件中:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Apple : NSObject
@property (nonatomic, copy) NSString *date;
@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:");
}
- (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
可以看到上方有两种方法可以注册监听,而且都是非常简洁好用。
接着我们就分析一下这个库的好用的原因:
- 统一观察者
在FBKVOController
中有个单例[_FBKVOSharedController sharedController]
来统一观察所有的对象,所有的观察回调也都先来到_FBKVOSharedController
中的observeValueForKeyPath:ofObject: change:context:
方法中,然后再派发给对应KVOController
的Block
或者Action(Seletor)
。
+ (instancetype)sharedController
{
static _FBKVOSharedController *_controller = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_controller = [[_FBKVOSharedController alloc] init];
});
return _controller;
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
// lookup context in registered infos, taking out a strong reference only if it exists
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
if (nil != info) {
// take strong reference to controller
FBKVOController *controller = info->_controller;
if (nil != controller) {
// take strong reference to observer
id observer = controller.observer;
if (nil != observer) {
// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary<NSString *, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
在添加观察者的时候,会把添加的信息生成一个_FBKVOInfo
对象。
@interface _FBKVOInfo : NSObject
@end
@implementation _FBKVOInfo
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
_FBKVOInfoState _state;
}
- 移除观察者时机 在使用
FBKVOController
添加观察者的时候会动态关联对象,该对象的类为FBKVOController
,而在类释放的时候,会调用类的delloc
方法,关联的对象也会走delloc
方法,而在这个时候统一去移除观察者。
- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}
- (void)_unobserveAll
{
// lock
pthread_mutex_lock(&_lock);
NSMapTable *objectInfoMaps = [_objectInfosMap copy];
// clear table and map
[_objectInfosMap removeAllObjects];
// unlock
pthread_mutex_unlock(&_lock);
_FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
for (id object in objectInfoMaps) {
// unobserve each registered object and infos
NSSet *infos = [objectInfoMaps objectForKey:object];
[shareController unobserve:object infos:infos];
}
}
- 防止重复添加
在添加的时候会把_FBKVOInfo
对象存起来,再次添加的时候去比较,如果存在,就不继续添加,在判断重复的时候重写了_FBKVOInfo
中的hash
方法,即keyPath
重复就不继续添加。
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// check for info existence
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
// observation info already exists; do not observe it again
// unlock and return
pthread_mutex_unlock(&_lock);
//此处就是之前已经存在相应的_FBKVOInfo对象了,所以直接返回
return;
}
// lazilly create set of infos
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
// add info and oberve
[infos addObject:info];
// unlock prior to callout
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController] observe:object info:info];
}
hash
查找_FBKVOInfo
对象和判断找到的对象是否与之前的_FBKVOInfo
相等的方法如下:
- (NSUInteger)hash
{
return [_keyPath hash];
}
- (BOOL)isEqual:(id)object
{
if (nil == object) {
return NO;
}
if (self == object) {
return YES;
}
if (![object isKindOfClass:[self class]]) {
return NO;
}
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}
手动关闭/打开KVO
在被观察的类中,重写automaticallyNotifiesObserversForKey:
方法,对应被观察的key
返回NO
,这时候就会不再调用观察者的observeValueForKeyPath:ofObject: change:context:
方法:
/// 手动关闭KVO(具体实现的时候我们可以根据key来if else决定那些被监听的值需要手动监听,那些不需要自动监听,需要手动监听就返回NO,需要自动监听就返回YES)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
而在被观察的类中,成对的调用willChangeValueForKey
和didChangeValueForKey
,即使被观察的key
没有发生变化也会手动的触发KVO
的回调:
//手动调用这个方法来查看我们监听的值的变化情况
- (void)invokeKVO{
[self willChangeValueForKey:@"name"];
[self didChangeValueForKey:@"name"];
}
KVC
什么是KVC
定义在NSKeyValueCoding.h
中,是一个非正式的协议。KVC
提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量
在NSKeyValueCoding
中提供了KVC
通用的访问方法,分别是getter
方法valueForKey
和setter
方法setValue:forKey
,以及其衍生的keyPath
方法,这两个方法是各个类通用的。并且由KVC
提供默认的实现,我们也可以自己重写对应的方法来改变实现。
基础操作
KVC
主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。
在使用KVC
时,直接将属性名当作key
,并设置value
,即可对属性进行赋值:
_apple = [[Apple alloc] init];
//初始化设置_apple的属性值
self.apple.date = @"7Days!";
self.apple.arrayTest = [[NSMutableArray alloc] init];
[self.apple setValue:@"14Days!" forKey:@"date"];
NSLog(@"%@", self.apple.date);
打印结果:
多级访问
除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如对当前对象的address
属性的street
属性进行赋值。
KVC
进行多级访问时,类似于属性调用一样用点语法进行访问即可[myInformation setValue:@"123" forKeyPath:@"address.street"];
:
实际代码例子如下:
_apple = [[Apple alloc] init];
self.apple.littleApple = [[sonOfApple alloc] init];
[self.apple setValue:@66 forKeyPath:@"littleApple.flag"];
NSLog(@"%@", self.apple.littleApple.flag);
运行结果:
传参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
- 如果重写方法成了
查找顺序的例子如下:
//接口成员变量定义
@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;
NSLog(@"%@", [_apple valueForKey:@"cnt"]);
打印结果为:
我们可以看到11
是成员变量_cnt
的值,我们输入的key
是@"cnt"
,结果找到的是_cnt
的值,这个也符合了我们上方说的查找结果。
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;
}
}
以上就是本文对KVC
的全部总结。