什么是键值编码KVC,键路径是什么? 什么是键值观察KVO?
键值编码KVC:
键值编码是一种在NSKeyValueCoding非正式协议下使用字符串标志间接访问对象属性的一种机制,也就是访问对象变量的一种特殊的捷径。如果一个对象符合键值编码的约定,那么它的属性就可以通过一个准确的、唯一的字符串(键路径字符串)参数进行访问,类似于将所有对象看做字典Dictionary,键路径为key(实际为keypath),属性值即value,通过键路径访问属性值。键值编码的间接访问方式其实是传统实例变量的存取方法访问的一种替代,也就是另外一种可以访问对象变量的方法。其中注意键值编码可以暴力访问对象的任何变量,无论是否是pirvate私有类型的变量。
通常我们是通过存取方法来访问对象的属性的,getter方法返回属性的值,setter方法设置属性的值。对于实例对象,我们可以直接通过存取方法或者变量名来访问对象的属性,但是随着属性数量的增加和对象变量的嵌套深度增大,访问代码会随之增多。相比之下,通过键值编码就可以简洁而稳定的对所有属性进行访问。
键值编码是Cocoa框架中很基础的一个概念,像KVO键值观察、Cocoa绑定、Core Data等都是基于KVC的。
键路径:
键路径就是键值编码中某个属性的key,一个由连续键名组成的字符串,键名即属性名,键名之间用点隔开,用于指定一个连接在一起的对象性质序列。键路径使我们可以独立于模型实现的方式指定相关对象的性质。通过键路径,可以指定对象图中的一个任意深度的路径,使其指向相关对象的某个特定的属性。
键值观察KVO:
键值观察,是基于键值编码实现的一种观察者机制,提供了观察某一属性变化的监听方法,用来简化代码,优化逻辑和组织。
这里提供一个最基本的kvo和kvc的使用示例。假设有一个专业类Major,Major有一个专业名majorName私有属性;一个学生类Student,Student有一个姓名name私有属性,同时还有一个专业Major对象变量,这样就出现了简单的Major和Student的对象嵌套。Major和Student都继承自NSObject都符合键值编码约定,可以定义一个Student变量对其进行键值编码和键值观察:
// 1.专业类模型:Major.h
@interface Major : NSObject {
@private
NSString *majorName; // 私有实例变量专业名称
}
@end
// 2.学生类模型:Student.h
@interface Student : NSObject {
@private
NSString *name; // 私有实例变量姓名
}
@property (nonatomic, strong)Major *major; // 学生专业
// 3.kvc和kvo之间基本使用方法
#import "Student.h"
#import "Major.h"
@interface ViewController ()
@property (nonatomic, strong)Student *student;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化学生Student对象
_student = [[Student alloc] init];
// 初始化学生的专业Major对象
_student.major = [[Major alloc] init];
// 1.set: 通过KVC设置Student对象的值(可以强行访问private变量)
[_student setValue:@"Sam" forKey:@"name"];
[_student setValue:@"Computer Science" forKeyPath:@"major.majorName"];
// 2.get: 通过KVC读取Student对象的值(可以强行访问private变量)
NSLog(@"%@", [_student valueForKey:@"name"]);
NSLog(@"%@", [_student valueForKeyPath:@"major.majorName"]);
// 3.kvo:添加当前控制器为键路径major.majorName的一个观察者,如果major.majorName的值改变会通知当前控制器从而自动调用下面的observeValueForKeyPath函数,这里传递旧值和新值
[_student addObserver:self forKeyPath:@"major.majorName" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
// 3s后改变major.majorName的值
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[_student setValue:@"Software Engineer" forKeyPath:@"major.majorName"];
});
}
/**
* 监听keyPath值的变化
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqual:@"major.majorName"]) {
// 获取变化前后的值并打印
NSString *oldValue = [change objectForKey:NSKeyValueChangeOldKey];
NSString *newValue = [change objectForKey:NSKeyValueChangeNewKey];
NSLog(@"major.majorName value changed:oldValue:%@ newValue:%@", oldValue,newValue);
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
/**
* 移除观察者释放资源,防止资源泄漏
*/
- (void)dealloc {
[_student removeObserver:self forKeyPath:@"major.majorName"];
}
@end
可以看到‘major.majorName’就是一个由两个键连接起来的基本键路径,相当于用它可以直接访问属性的属性;使用addObserver函数将当前控制器注册为Student对象的观察者,当Student中键路径‘major.majorName’下的值发生改变时会通知当前控制器,触发observeValueForKeyPath监听函数。
NSNotification是同步还是异步? KVO是同步还是异步?
默认在主线程中通知是同步的,当通知产生时,通知中心会一直等待所有的观察者都收到并且处理通知结束,然后才会返回到发送通知的地方继续执行后面的代码;但可以将通知的发送或者将通知的处理方法放到子线程中从而避免通知阻塞。其中通知的发送可以添加到NSNotificationQueue异步通知缓冲队列中,也不会导致通知阻塞。NSNotificationQueue是一个通知缓冲队列,通常以FIFO先进先出的规则维护通知队列的发送,向通知队列添加通知有三种枚举类型:NSPostASAP、NSPostWhenIdle、和NSPostNow,分别表示尽快发送、空闲时发送和现在立刻发送,可以根据通知的紧急程度进行选择。
下面示例验证默认通知是同步的:
// 自定义消息的名称
#define MYNotificationTestName @"NSNotificationTestName"
// 1.注册通知的观察者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(process) name:MYNotificationTestName object:nil];
// 2.发出通知给观察者
NSLog(@"即将发出通知!");
[[NSNotificationCenter defaultCenter] postNotificationName:MYNotificationTestName object:nil];
NSLog(@"发出通知处的下一条代码!");
/**
* 3.处理收到的通知
*/
- (void)process {
sleep(10); // 假设处理需要10s
NSLog(@"通知处理结束!");
}
打印结果:
2017-01-21 22:21:30.501 SingleView[4579:146073] 即将发出通知!
2017-01-21 22:21:40.572 SingleView[4579:146073] 通知处理结束!
2017-01-21 22:21:40.572 SingleView[4579:146073] 发出通知处的下一条代码!
打印“即将发出通知”后,等了10s之后才打印出“通知处理结束”,然后才打印出@“发出通知处的下一条代码!”,“发出通知处的下一条代码!”是等到通知处理结束才打印出来的,说明通知是同步的。
可以通过将通知的发送语句或者通知的处理语句放到子线程实现通知的异步:
将通知的发送语句放到子线程:
NSLog(@"即将发出通知!");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:MYNotificationTestName object:nil];
});
NSLog(@"发出通知处的下一条代码!");
或者:
NSLog(@"即将发出通知!");
// 将通知放到通知异步缓冲队列
NSNotification *notification = [NSNotification notificationWithName:MYNotificationTestName object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
NSLog(@"发出通知处的下一条代码!");
将通知的处理放到子线程:
/**
* 处理收到的通知
*/
- (void)process {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(10); // 假设处理需要10s
NSLog(@"通知处理结束!");
});
}
执行结果变为:
2017-01-21 22:31:09.259 SingleView[4711:151180] 即将发出通知!
2017-01-21 22:31:09.260 SingleView[4711:151180] 发出通知处的下一条代码!
2017-01-21 22:31:19.290 SingleView[4711:151252] 通知处理结束!
类似的,KVO也是同步的。