KVC — Key Value Coding 键值编码

KVC 是一种不用通过调用 setter、getter 方法而是直接通过属性字符串名称key来存取属性的机制。

KVC 和 属性访问器的对比如下:

  • KVC是通过在运行时动态的访问和修改对象的属性而访问器是在编译时确定,单就这一点增加了访问属性的灵活性,
    但是用访问器访问属性的时候编译器会做预编译处理,访问不存在的属性编译器会报错,使用KVC方式的时候如果有错误只能在运行的时候才能发现。

  • 相比访问器KVC 效率会低一点。

  • KVC 可以访问对象的私有属性,修改只读属性

  • KVC 在字典转模型,集合类操作方面有着普通访问器不能提供的功能

总而言之 KVC的特点是在运行时访问,可以访问对象私有属性和修改只读属性,在字典转模型,集合类操作方面十分便捷。

首先先列下关键的一些方法,后续都会对这些方法进行介绍:

//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;

//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;

//默认返回YES,表示如果没有找到会按照一定的规则进行搜索,设置成NO如果没有找到就当作没有,会抛出异常。
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。但是注意这个方法是不会自动调用的需要我们手动调用。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一个方法一样,但这个方法是设值,前面一个是取值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

// 容器类型相关
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

// 传入key数组, 返回一个成员变量名和变量值的键值对组成的字典(可用于模型转字典)
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
// 字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

下面是KVC 关键用法的总结:

1. 设置和访问策略及访问私有属性和可读属性
#import <Foundation/Foundation.h>

@interface Teacher : NSObject
@property (nonatomic, strong, readonly) NSString *readonlyValue;
@end

#import "Teacher.h"

@interface Teacher ()

@property (nonatomic, assign ,readwrite) NSInteger age;
@property (nonatomic, strong ,readwrite) NSString *name;
@property (nonatomic, assign ,readwrite) BOOL male;
@property (nonatomic, assign ,readwrite) BOOL isTest;

@end

@implementation Teacher

- (NSString *)description {
return [NSString stringWithFormat:@"name = %@ \
age = %ld \
readOnlyValue = %@ \
is Male = %d",
self.name, (long)self.age, self.readonlyValue, self.male];
}

int main(int argc, const char * argv[]) {
@autoreleasepool {

Teacher *teacher = [Teacher new];
[teacher setValue:@"linxiaohai" forKey:@"name"];
NSLog(@"%@",teacher);
[teacher setValue:@"linxiaohai1" forKey:@"_name"];
NSLog(@"%@",teacher);

//修改NSInteger要转换成NSNumber*
[teacher setValue:@(29) forKey:@"age"];
NSLog(@"%@",teacher);

//修改BOOL要转换成NSNumber*
[teacher setValue:@(YES) forKey:@"male"];
NSLog(@"%@",teacher);
[teacher setValue:@(NO) forKey:@"_male"];
NSLog(@"%@",teacher);

//修改只读属性
[teacher setValue:@"This is a readOnly Value" forKey:@"readonlyValue"];
NSLog(@"%@",teacher);
[teacher setValue:@"This is a readOnly Value 2" forKey:@"_readonlyValue"];
NSLog(@"%@",teacher);

//如果没有找到将会按照_is<Key> isKey 方式继续查找
NSLog(@"Test: %d", [[teacher valueForKey:@"test"] boolValue]);
[teacher setValue:@(YES) forKey:@"test"];
NSLog(@"%@",teacher);
NSLog(@"Test: %d", [[teacher valueForKey:@"test"] boolValue]);
}
return 0;
}

2019-06-21 11:27:36.860598+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 0 readOnlyValue = (null)   is Male = 0
2019-06-21 11:27:36.860744+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 0
2019-06-21 11:27:36.860916+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 0
2019-06-21 11:27:36.861087+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 1
2019-06-21 11:27:36.861213+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value is Male = 1
2019-06-21 11:27:36.861348+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value 2 is Male = 1
2019-06-21 11:27:36.861422+0800 KVC-Demo[42518:5605320] male: 1
2019-06-21 11:27:36.877241+0800 KVC-Demo[42518:5605320] ===================================================
2019-06-21 11:27:36.877501+0800 KVC-Demo[42518:5605320] Test: 0
2019-06-21 11:27:36.877765+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value 2 is Male = 1
2019-06-21 11:27:36.877824+0800 KVC-Demo[42518:5605320] Test: 1
2019-06-21 11:27:36.877867+0800 KVC-Demo[42518:5605320] ===================================================

  • -(void)setValue:(id)value forKey:(NSString *)key 规则:
首先搜索 setter 方法,有就直接赋值。
如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
返回 NO,则执行setValue:forUNdefinedKey:
返回 YES,则按_<key>,_<isKey><key><isKey>的顺序搜索[实例变量]注意这里是搜索实例变量,不是属性。
还没有找到的话,就调用setValue:forUndefinedKey:
总结来说就是KVC设值的顺序如下: setKey -> _key -> _isKey -> key -> isKey
  • -(id)valueForKey:(NSString *)key 规则
* 首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。
* 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
* 返回 NO,则执行valueForUNdefinedKey:
* 返回 YES,则按_<key>,_is<Key>,<key>,is<Key>的顺序搜索实例变量。
* 如果没有则查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法,如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合
* 还没找到则查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法,如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合。
* 还没有找到的话,调用valueForUndefinedKey:
总结来说就是KVC取值的顺序如下: getKey -> _key -> _isKey -> key -> isKey
-> [countOf<Key> objectIn<Key>AtIndex <Key>AtIndexes] -> [countOf<Key>,enumeratorOf<Key>,memberOf<Key>]

keyPath 用法:

Teacher *teacher = [Teacher new];
Student *student = [Student new];
[student setValue:@"Jimmy" forKey:@"name"];
[teacher setValue:student forKey:@"student"];
NSString *studentName = [teacher valueForKeyPath:@"student.name"];
NSLog(@"Student name: %@",studentName);
[teacher setValue:@"linxiaohai" forKeyPath:@"student.name"];
2. 异常处理

如果都没找到对应的key就会调用setValue:forUndefinedKey: 和valueForUndefinedKey:
下面是一个例子:

增加
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if([key isEqualToString:@"undefineKey"]) {
NSLog(@"=========> %@",value);
}
}

在调用[teacher setValue:@"valueForUndefineKey" forKey:@"undefineKey"] 的时候就会打出如下log

=========> valueForUndefineKey


增加:
- (id)valueForUndefinedKey:(NSString *)key {
return [NSString stringWithFormat:@"This is A default Value for %@",key];
}

在调用
NSLog(@"Test: %@", [teacher valueForKey:@"undefineKey"]); 就会打出如下Log:

Test: This is A default Value for undefineKey

其他API

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回

// 如果你在setValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
3. 字典转模型

使用字典来初始化模型

NSDictionary *catogiresDic = @{
@"id" :@1,
@"image":@"@www.iPhone.com",
@"name":@"iPhone",
@"price":@"100.03"};
CategoryList *catogires = [[CategoryList alloc] init];
[catogires setValuesForKeysWithDictionary:catogiresDic];

通过key的集合来从一个model中取出值形成一个字典

NSDictionary *value = [catogires dictionaryWithValuesForKeys:@[@"name",@"image"]];
4. 集合操作

KVC对于数组而言最大的功能还是获取集合类的 count,max,min,avg,sum 这是一个很好用的功能

集合操作的keyPath整体格式如下图所示:

左边是当前对象中待操作的集合实例对象,如果是针对自身的某个属性做操作,则可以省略,”@”操作符右边的是需要操作的属性。如果操作符是count的话右边也可以省略,这时候相当于针对当前数量统计。

  • 简单运算符
NSLog(@"count of book price : %@",[student valueForKeyPath:@"bookList.@count"]);
NSLog(@"count of book price : %@",[student valueForKeyPath:@"bookList.@count.price"]);
NSLog(@"min of book price : %@",[student valueForKeyPath:@"bookList.@min.price"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.@max.price"]);
NSLog(@"sum of book price : %@",[student valueForKeyPath:@"bookList.@sum.price"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.@avg.price"]);

如果自身是一NSNumber数组的话右边的可以为self,表示针对的是自身

NSLog(@"count of book price : %@",[student valueForKeyPath:@"@count"]);
NSLog(@"count of book price : %@",[student valueForKeyPath:@"@count.self"]);
NSLog(@"min of book price : %@",[student valueForKeyPath:@"bookList.self"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.self"]);
NSLog(@"sum of book price : %@",[student valueForKeyPath:@"bookList.self"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.self"]);
  • 对象运算符

通过这个特性能够一个对象集合的所有某个特定字端的值组成一个数组

一共有两种:

@distinctUnionOfObjects
@unionOfObjects

它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。例子如下:

NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
  • 数组集合运算符

这类操作符的操作对象内对象必须是数组/集合,也就是元素为数组/集合的数组/集合:下面是一个简单例子:

IDLProductModel *productA = [[IDLProductModel alloc] init];
productA.price = 99.0;
productA.name = @"iPod";

IDLProductModel *productB = [[IDLProductModel alloc] init];
productB.price = 199000.0;
productB.name = @"iMac";

IDLProductModel *productC = [[IDLProductModel alloc] init];
productC.price = 2990.0;
productC.name = @"iPhone";

IDLProductModel *productD = [[IDLProductModel alloc] init];
productD.price = 1990.0;
productD.name = @"iPhone";

NSArray *product = @[productA, productB, productC, productD];
NSArray *distinctUnionOfArrays = [@[product, product] valueForKeyPath:@"@distinctUnionOfArrays.price"];
NSArray *unionOfArrays = [@[product, product] valueForKeyPath:@"@unionOfArrays.price"];

NSSet *setA = [NSSet setWithObjects:productA, productB, nil];
NSSet *setB = [NSSet setWithObjects:productC, productD, nil];
NSSet *set = [NSSet setWithObjects:setA, setB, nil];

NSSet *allSet = [set valueForKeyPath:@"@distinctUnionOfSets.name"];

这种其实就是将操作对象的所有数据取出来,展开铺平为一个数组后,再对数组或者集合做操作。

5. 键值验证

KVC提供了验证Key对应的Value是否可用的方法,但是这个方法不会自动调用,必须在使用的时候手动调用:

- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;

如下例子:

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
NSNumber *age = *ioValue;
if (age.integerValue == 10) {
return NO;
}
return YES;
}

NSNumber *age = @10;
NSError* error;
NSString *key = @"age";
BOOL isValid = [test validateValue:&age forKey:key error:&error];

6.其他用途
  • 指定数组中每个元素的执行方法,然后将新元素放到新数组中返回
NSArray *arr = @[@"iPod", @"iPhone", @"iMac", @"iPhone8 Plus"];
NSArray *uppercaseStrArr = [arr valueForKeyPath:@"uppercaseString"];
  • 抽取字典数组中的指定key
NSArray *array = @[@{@"name" : @"iPod",   @"price" : @99 },
@{@"name" : @"iPhone", @"price" : @199},
@{@"name" : @"iPhone", @"price" : @299},
@{@"name" : @"iPhone", @"price" : @299},
];
NSLog(@"%@", [array valueForKeyPath:@"name"]);//iPod,iPhone,iPhone,iPhone
KVO — Key Value Observer 键值观察
1. KVO 的基本使用

KVO 适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO 只能对属性做出反应,不会用来对方法或者动作做出反应

为某个对象的属性添加观察者:

[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior
context:nil];

回调方法

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
NSLog(@"%@对象的%@属性改变了:%@",object,keyPath,change);
}

移除观察者

[self.person removeObserver:self forKeyPath:@"age"];

这里比较重要的是option这个参数:

NSKeyValueObservingOptionNew = 0x01 change字典包括改变后的值
NSKeyValueObservingOptionOld = 0x02 change字典包括改变前的值
NSKeyValueObservingOptionInitial = 0x04 注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior = 0x08
如果指定,则在每次修改属性时,会在修改通知被发送之前预先发送一条通知给观察者,
这与-willChangeValueForKey:被触发的时间是相对应的。
这样,在每次修改属性时,实际上是会发送两条通知。
2. KVO 触发规则:
  • 通过成员变量的setter方法/或者通过KVC都可以触发KVO
  • 直接修改成员变量是不会触发KVC的,需要手动添加KVC才能触发。如何手动触发见下面部分。
3. 设置相互关联的属性

假如有个 Person 类,类里有三个属性,fullName、firstName、lastName。这种情况如果需要观察名字的变化,就要分别添加 fullName、firstName、lastName 三次观察,非常麻烦。通过设置相互关联的属性就会在关联的属性发生变化的时候,另外的属性也受到通知。

@interface Person : NSObject

@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@property (nonatomic, strong) NSString *fullName;

@end

@implementation Person

//在这里设置与fullName相关的依赖属性,外面一旦设置了这些依赖属性,fullName的观察者就会被触发
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}

@end

[_person addObserver:self
forKeyPath:@"fullName"
options:NSKeyValueObservingOptionNew
context:nil];

[_person setValue:@"FistName" forKey:@"fistName"];

上面的例子中我们不论设置Person类的firstName或者lastName 都会触发 fullName 的观察。

4. 手动KVO (禁用自动KVO)
@interface Target : NSObject {
int age;
}

- (int)age;
- (void)setAge:(int)theAge;

@end

@implementation Target

- (id)init {
self = [super init];
if (nil != self) {
age = 10;
}
return self;
}

- (int)age {
return age;
}

- (void)setAge:(int)theAge {
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}

@end
  1. 需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法
  2. 实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
5. 查看某个对象有哪些属性被监听

如果我们想获取一个对象上有哪些观察者正在监听其属性的修改,则可以查看对象的observationInfo属性

id info = object.observationInfo;
NSLog(@"%@", [info description]);
6.KVO 原理:
KVO 是通过 isa-swizzling 实现的,当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中做如下的几方面工作:

1. 重写基类中任何被观察属性的 setter 方法,而通过重写就获得了 KVO 需要的通知机制

- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; // KVO在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; // 调用父类的存取方法
[self didChangeValueForKey:@"name"]; // KVO在调用存取方法之后总调用
}

KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。 当改变发生后,didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更。 之后,observeValueForKey:ofObject:change:context: 也会被调用。

2. 让这个重写的类成为原来类的派生类:

除了完成上面提到的第一步操作的同时派生类还重写了class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制

3. 重写派生类的dealloc 方法释放资源

扩展阅读

*KVO进阶——KVO实现探究

Contents
  1. 1. KVC — Key Value Coding 键值编码
    1. 1.1. 1. 设置和访问策略及访问私有属性和可读属性
    2. 1.2. 2. 异常处理
    3. 1.3. 3. 字典转模型
    4. 1.4. 4. 集合操作
    5. 1.5. 5. 键值验证
    6. 1.6. 6.其他用途
  2. 2. KVO — Key Value Observer 键值观察
    1. 2.1. 1. KVO 的基本使用
    2. 2.2. 2. KVO 触发规则:
    3. 2.3. 3. 设置相互关联的属性
    4. 2.4. 4. 手动KVO (禁用自动KVO)
    5. 2.5. 5. 查看某个对象有哪些属性被监听
    6. 2.6. 6.KVO 原理:
  • 扩展阅读