KVC
KVC(Key-value coding)
键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。
KVC
的定义都是对NSObject
的扩展来实现的,Objective-C
中有个显式的NSKeyValueCoding
类别名,所以对于所有继承了NSObject
的类型,都能使用KVC
(一些纯Swift
类和结构体
是不支持KVC
的,因为没有继承NSObject
),下面是KVC
最为重要的四个方法:
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
NSKeyValueCoding
还定义了其他的一些方法:
+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值正确性�验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
KVC的相关概念
KVC的赋值
KVC
如果要赋值的话首先就需要对象对应的Key
,那么KVC
的内部是怎么工作的呢,当我们使用setValue:forKey:
的代码时,底层的执行机制顺序如下:
1.程序首先通过
setter
方法进行赋值,比如我们的key
是@"name"
,则会调用setName:
方法。
2.如果没有找到
setName:
方法,KVC
机制会检查+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回YES
,默认该方法会返回YES
,如果你重写了该方法让其返回NO
的话,那么在这一步KVC
会执行setValue:forUndefinedKey:
方法。
3.如果我们没有设置
accessInstanceVariablesDirectly
返回NO
,KVC
机制会搜索该类里面有没有名为name
的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只要存在以name
命名的变量,KVC
都可以对该成员变量赋值。
4.如果该类即没有
setName:
方法,也没有_name
成员变量,KVC
机制会搜索_isName
的成员变量。
5.和上面一样,如果该类即没有
setName:
方法,也没有_name
和_isName
成员变量,KVC
机制再会继续搜索name
和isName
的成员变量。再给它们赋值。
6.如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的
setValue:forUndefinedKey:
方法,默认是抛出异常。
让我们用代码一一来说明:
//test类中声明成员变量
#import "testKVC.h"
@interface testKVC()
{
NSString *_name;
}
@end
@implementation testKVC
@end
//执行文件中为_name赋值
testKVC *obj = [[testKVC alloc] init];
[obj setValue:@"MichealMIX" forKey:@"name"];
NSLog(@"obj的成员变量name的值为:%@",[obj valueForKey:@"name"]);
//控制台输出
2020-01-09 20:28:55.576794+0800 KVC&KVO[8003:713495] obj的成员变量name的值为:MichealMIX
通过- (void)setValue:(nullable id)value forKey:(NSString *)key;
和- (nullable id)valueForKey:(NSString *)key;
成功设置和取出obj
对象的name
值。
再看一下设置accessInstanceVariablesDirectly
为NO
的效果:
#import "testKVC.h"
@interface testKVC()
{
NSString *_name;
}
@end
@implementation testKVC
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}
//取值出现错误
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出现异常,名为%@的key不存在",key);
return nil;
}
//赋值出现错误
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出现异常,名为%@的key不存在", key);
}
@end
//控制台输出
2020-01-09 20:35:56.716841+0800 KVC&KVO[8082:718165] 出现异常,名为name的key不存在
2020-01-09 20:35:56.717084+0800 KVC&KVO[8082:718165] 出现异常,名为name的key不存在
2020-01-09 20:35:56.717201+0800 KVC&KVO[8082:718165] obj的成员变量name的值为:(null)
可以看到如果accessInstanceVariablesDirectly
返回值设置为NO
,那么只会查找getter
和setter
这一层,而不会继续寻找。
我们将accessInstanceVariablesDirectly
设置为YES
,然后设置成员变量名字为isName
,看看能否成功赋值取值。
#import "testKVC.h"
@interface testKVC()
{
NSString *isName;
}
@end
@implementation testKVC
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
//取值出现错误
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出现异常,名为%@的key不存在",key);
return nil;
}
//赋值出现错误
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出现异常,名为%@的key不存在", key);
}
@end
//控制台输出
2020-01-09 20:51:31.572195+0800 KVC&KVO[8157:723618] obj的成员变量name的值为:MichealMIX
KVC的取值
当调用valueForKey:
的代码时,KVC
对key
的搜索方式不同于setValue:forKey:
,我们假设成员变量为age
,按getAge
,age
,isAge
的顺序方法查找getter
方法,找到的话会直接调用。如果是BOOL
或者Int
等值类型, 会将其包装成一个NSNumber
对象。
代码来验证一下:
#import "testKVC.h"
@interface testKVC()
@end
@implementation testKVC
- (NSUInteger)getAge {
return 10;
}
@end
//调用取值方法
testKVC *obj = [[testKVC alloc] init];
NSLog(@"年龄的值为:%@",[obj valueForKey:@"age"]);
//控制台输出
2020-01-10 09:39:05.558566+0800 KVC&KVO[8628:742152] 年龄的值为:10
再来验证一下age
和isAge
的取值方式:
#import "testKVC.h"
@interface testKVC()
@end
@implementation testKVC
- (NSUInteger)age {
return 10;
}
//或者
- (NSUInteger)isAge {
return 10;
}
@end
//调用取值方法
testKVC *obj = [[testKVC alloc] init];
NSLog(@"年龄的值为:%@",[obj valueForKey:@"age"]);
//控制台输出
2020-01-10 09:39:05.558566+0800 KVC&KVO[8628:742152] 年龄的值为:10
这些方式都可以找到age
的值,也就验证了我们之前的定义。
KeyPath
一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC
获取该属性,然后再次用KVC
来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC
提供了一个解决方案,那就是键路径keyPath
。顾名思义,就是按照路径寻找key
。
先看一下代码:
//定义一个test类,含有成员变量name
#import "test.h"
@interface test()
{
NSString *name;
}
@end
@implementation test
@end
//再定义一个testKVC类,其含有成员变量test
#import "testKVC.h"
#import "test.h"
@interface testKVC()
{
test*testKeyPath;
}
@end
@implementation testKVC
@end
//调用函数中为test类中的name赋值
testKVC *obj = [[testKVC alloc] init];
test *obj1 = [[test alloc] init];
[obj setValue:obj1 forKey:@"testKeyPath"];
[obj setValue:@"MichealMIX" forKeyPath:@"testKeyPath.name"];
NSLog(@"test的成员变量name的值为:%@",[obj valueForKeyPath:@"testKeyPath.name"]);
//控制台输出
2020-01-10 10:12:39.293053+0800 KVC&KVO[9066:762997] test的成员变量name的值为:MichealMIX
KVC
对于keyPath
是搜索机制第一步就是分离key
,用小数点.来分割key
,然后再像普通key
一样按照先前介绍的顺序搜索下去。
KVC处理异常
KVC处理nil
通常情况KVC
是不允许你在赋值的时候传nil
的,我们可以重写一个方法,防止在不小心的情况下误传了nil
造成不必要的问题。
//testKVC类
#import "testKVC.h"
@interface testKVC()
{
NSUInteger age;
}
@end
@implementation testKVC
//重写方法
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能将%@设成nil", key);
}
@end
//调用赋值函数
testKVC *obj = [[testKVC alloc] init];
[obj setValue:nil forKey:@"age"];
//控制台输出
2020-01-10 10:42:58.180137+0800 KVC&KVO[9307:777429] 不能将age设成nil
KVC键值验证
我们可以使用validateValue
来验证赋值的合法性,比如一些变量的值要大于0,我们就可以使用键值验证。
#import "testKVC.h"
@interface testKVC()
{
NSUInteger age;
}
@end
@implementation testKVC
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
NSNumber *age = *ioValue;
if (age.integerValue < 0) {
return NO;
}
return YES;
}
@end
//调用方法
testKVC *obj = [[testKVC alloc] init];
NSNumber *age = [NSNumber numberWithLong:-1];
NSError* error;
NSString *key = @"age";
BOOL isValid = [obj validateValue:&age forKey:key error:&error];
if (isValid) {
NSLog(@"赋值合法");
[obj setValue:age forKey:key];
}
else {
NSLog(@"赋值不合法");
}
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [obj valueForKey:@"age"]);
//控制台输出
2020-01-10 10:54:30.785211+0800 KVC&KVO[9397:783600] 赋值不合法
2020-01-10 10:54:30.785501+0800 KVC&KVO[9397:783600] test的年龄是0
KVC简单的集合处理
简单集合运算符共有@avg, @count , @max , @min ,@sum
5种,从字义就能看出来他们分别代表什么了,不支持自定义。
//Person类
@interface Person : NSObject
@property(nonatomic,strong)NSString *name;
@property(nonatomic,assign)int age;
@end
#import "Person.h"
@implementation Person
@end
//调用函数
- (void)main {
Person *tom = [[Person alloc] init];
tom.name = @"Tom";
tom.age = 20;
Person *jim = [[Person alloc] init];
jim.name = @"Jim";
jim.age = 10;
Person *steve = [[Person alloc] init];
steve.name = @"Steve";
steve.age = 15;
Person *james = [[Person alloc] init];
james.name = @"James";
james.age = 30;
Person *jane = [[Person alloc] init];
jane.name = @"Jane";
jane.age = 33;
NSArray* arrBooks = @[tom,jim,steve,james,jane];
NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.age"];
NSLog(@"sum:%d",sum.intValue);
NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.age"];
NSLog(@"avg:%d",avg.intValue);
NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
NSLog(@"count:%d",count.intValue);
NSNumber* min = [arrBooks valueForKeyPath:@"@min.age"];
NSLog(@"min:%d",min.intValue);
NSNumber* max = [arrBooks valueForKeyPath:@"@max.age"];
NSLog(@"max:%d",max.intValue);
}
//控制台输出
2020-01-10 11:17:01.715837+0800 KVC&KVO[9616:797516] sum:108
2020-01-10 11:17:01.716080+0800 KVC&KVO[9616:797516] avg:21
2020-01-10 11:17:01.716224+0800 KVC&KVO[9616:797516] count:5
2020-01-10 11:17:01.716345+0800 KVC&KVO[9616:797516] min:10
2020-01-10 11:17:01.716456+0800 KVC&KVO[9616:797516] max:33
KVC处理字典
KVC
中关于处理字典的两个方法:
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
我们通过代码来看一下这些方法会更加直观:
//模型类
@interface Address : NSObject
@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;
@end
//运行函数
- (void)main {
Address *address = [[Address alloc] init];
address.country = @"China";
address.province = @"Guang Dong";
address.city = @"ShenZhen";
address.district = @"Nanshan";
NSArray *adressArray = @[@"country",@"province",@"city",@"district"];
//将模型转换为字典
NSDictionary *dict = [address dictionaryWithValuesForKeys:adressArray];
NSLog(@"KVC建立的字典%@",dict);
//字典转模型
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[address setValuesForKeysWithDictionary:modifyDict]; //用key Value来修改Model的属性
NSLog(@"country:%@ province:%@ city:%@",address.country,address.province,address.city);
}
//控制台输出
2020-01-10 13:54:19.101262+0800 KVC&KVO[9883:816765] KVC建立的字典{
city = ShenZhen;
country = China;
district = Nanshan;
province = "Guang Dong";
}
2020-01-10 13:54:19.101446+0800 KVC&KVO[9883:816765] country:USA province:california city:Los angle
KVC的使用
1.动态的取值和赋值
2.用KVC
来访问或者修改私有变量
3.model
和字典的互相转换
4.修改一些控件的内部属性
KVO
KVO
就是基于之前的KVC
来实现的,我们来看看什么是KVO
,KVO 即 Key-Value Observing
,翻译成键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察者,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,自动的通知观察者。
简单来说KVO
可以通过监听key
,来获得value
的变化,用来在对象之间监听状态变化。KVO
的定义都是对NSObject的扩展来实现的,Objective-C
中有个显式的NSKeyValueObserving
类别名,和KVC
一样swift
等类无法使用。
首先我们通过代码简单看一下KVO是如何实现的:
//定义一个类,在.h文件声明一个成员变量及其存取方法
@interface TestKVO : NSObject
{
int number;
}
- (void)setNumber:(int)theNum;
- (int)number;
@end
//.m文件
#import "TestKVO.h"
@implementation TestKVO
- (instancetype)init{
self = [super init];
if (self != nil) {
//初始化number为100
number = 100;
}
return self;
}
- (void)setNumber:(int)theNum{
number = theNum;
}
- (int)number{
return number;
}
@end
//调用函数
- (void)main{
kvo = [[TestKVO alloc] init];
[kvo addObserver:self forKeyPath:@"number" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
[kvo setNumber:150];
}
//回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"change = %@",change);
if ([keyPath isEqualToString:@"number"] && object == kvo) {
}
}
//移除观察者
- (void)dealloc {
[kvo removeObserver:self forKeyPath:@"number" context:nil];
}
//控制台打印
2020-01-10 15:24:12.228724+0800 KVC&KVO[10498:854550] change = {
kind = 1;
new = 150;
old = 100;
}
可以看到KVO
成功的监听了成员变量number
的值,并且返回给我们旧值以及改变后的值。
首先我们想使用KVO
一定要了解一下两个方法:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
我们要为想观察的对象添加观察者,当完成观察后一定要移除观察者,否则会造成内存泄漏。
再来看一下这两个方法中的参数以及重要的回调方法:
observer:观察者,也就是KVO通知的订阅者。必须实现
observeValueForKeyPath:ofObject:change:context:方法,它会返回给我们记录前后变化的字典
keyPath:描述将要观察的属性,相对于被观察者。
options:KVO的一些属性配置;有四个选项。
context: 上下文,这个会传递到订阅着的函数中,用来区分消息,所以应当是不同的。
options中的内容:
NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)
这里还要提到一点,有一些情况下我们需要手动进行KVO,比如一些类库不希望我们KVO,这时候就又有两个重要的方法出现了:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
KVO
的自动实现就是隐式的调用了这两个方法来帮我们记录值的变化,并且它会进行通知,而手动实现KVO
就需要我们显示的调用了,由于我们手动调用这两个方法,那我们就要将值发生变化的自动通知关掉。
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
来看一下实现手动KVO
的代码:
//在刚才的类中新增一个age成员变量
@interface TestKVO : NSObject
{
int age;
int number;
}
- (void)setAge:(int)theAge;
- (int)age;
- (void)setNumber:(int)theNum;
- (int)number;
@end
//.m
#import "TestKVO.h"
@implementation TestKVO
- (instancetype)init{
self = [super init];
if (self != nil) {
age = 10;
number = 100;
}
return self;
}
- (void)setAge:(int)theAge{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
- (int)age{
return age;
}
- (void)setNumber:(int)theNum{
number = theNum;
}
- (int)number{
return number;
}
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
//关闭对键age的自动通知
if ([key isEqualToString:@"age"]) {
return NO;
}
//其他照旧
return [super automaticallyNotifiesObserversForKey:key];
}
@end
//调用函数
- (void)main{
kvo = [[TestKVO alloc] init];
[kvo addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
[kvo setAge:15];
}
//控制台打印
2020-01-10 16:16:17.990038+0800 KVC&KVO[10737:871671] change = {
kind = 1;
new = 15;
old = 10;
}
可以看到,我们关闭自动KVO
,手动执行变化通知也可以打印出记录变化的结果,不要忘记要移除观察者,以免内存泄漏。
注意!
一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列
或者 Run-loop
的处理。手动或者自动调用-didChange...
会触发 KVO
通知。
所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO
和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用KVO
。
KVO的实现
KVO 是通过
isa-swizzling
实现的。
编译器自动为被观察对象创造一个派生类
,并将被观察对象的isa
指向这个派生类
。如果用户注册了对此目标对象的某一个属性的观察,那么此派生类会重写setter方法,并在其中添加进行通知的代码。Objective-C
在发送消息的时候,会通过isa
指针找到当前对象所属的类对象。而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了override
,并添加了通知代码,因此会向注册的对象发送通知。注意派生类
只重写注册了观察者的属性方法。
KVO
的原理是修改setter
方法,因此使用KVO
必须调用setter
。若直接访问属性对象则没有效果。