文章目錄
  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. 键值验证
  2. 2. KVO — Key Value Observer 键值观察
    1. 2.1. KVO 的使用
    2. 2.2. 设置相互关联的属性
    3. 2.3. KVO 原理:
    4. 2.4. 手动KVO (禁用自动KVO)
    5. 2.5. 查看某个对象有哪些属性被监听
KVC — Key Value Coding 键值编码

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

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

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

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

  • KVC 可以访问对象的私有属性,修改只读属性
  • KVC 在字典转模型,集合类操作方面有着普通访问器不能提供的功能

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

下面是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:
  • (id)valueForKey:(NSString *)key 规则
首先查找 getter 方法,找到直接调用。如果是 boolintfloat 等基本数据类型,会做 NSNumber 的转换。
如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
返回 NO,则执行valueForUNdefinedKey:
返回 YES,则按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员名。
还没有找到的话,调用valueForUndefinedKey:

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. 集合操作

相比于普通对象NSArray 以及 NSSet 在使用上我们需自己实现一遍下面的方法:

//必须实现,对应于NSArray的基本方法count: 
-countOf<Key>
//这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-objectIn<Key>AtIndex:
-<Key>AtIndexes:

//两个必须实现一个,类似 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes:

//两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes:

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

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"]);

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

一共有两种:

@distinctUnionOfObjects
@unionOfObjects

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

NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
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];
KVO — Key Value Observer 键值观察
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:被触发的时间是相对应的。
这样,在每次修改属性时,实际上是会发送两条通知。
设置相互关联的属性

假如有个 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

+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}

@end

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

[_person setValue:@"FistName" forKey:@"fistName"];
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)
@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 来处理。
查看某个对象有哪些属性被监听

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

id info = object.observationInfo;
NSLog(@"%@", [info description]);
文章目錄
  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. 键值验证
  2. 2. KVO — Key Value Observer 键值观察
    1. 2.1. KVO 的使用
    2. 2.2. 设置相互关联的属性
    3. 2.3. KVO 原理:
    4. 2.4. 手动KVO (禁用自动KVO)
    5. 2.5. 查看某个对象有哪些属性被监听