前言:KVC和KVO在项目开发中很常用,所以还是有必要深入了解一下。本文先介绍了KVC的定义和常见的API及使用方法,并介绍了setValue: forKey和valueForKey的底层调用顺序。然后介绍了KVO常见的用法和注意事项;最后说明了一些面试官喜欢面试的KVC和KVO的细节点。
一、KVC-键值编码:
1、KVC定义:
KVC,Key-Value Coding,键值编码,开发者可以通过key直接访问对象的属性,或者给对象的属性赋值,是iOS中的黑魔法之一。
2、常见的API:
- (void)setValue:(id)value forKey:(NSString *)key; //根据属性名赋值
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath; //根据路径赋值
- (void)setValue:(id)value forUndefinedKey:(NSString *)key; //赋值找不到属性
- (id)valueForKey:(NSString *)key; //根据属性名取值
- (id)valueForKeyPath:(NSString *)keyPath; //根据路径取值
- (id)valueForUndefinedKey:(NSString *)key; //取值找不到属性
代码如下:先创建一个Person类和Dog类,
@class Dog;
@interface Person : NSObject
{
@public
int _age;
}
@property(nonatomic, assign) int age;
@property(nonatomic, copy) NSString *name;
@property(nonatomic, strong) Dog *dog;
@end
@interface Dog : NSObject
@property(nonatomic, copy) NSString *name;
@end
运行代码:
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property(nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [Person new];
self.person.dog = [Dog new];
// 如果key不存在,则抛出NSUnknownKeyException异常,可以重写UndefinedKey的两个方法
[self.person setValue:@"18" forKey:@"age"];
[self.person setValue:@"旺财" forKeyPath:@"dog.name"];
int age = [[self.person valueForKey:@"age"] intValue];
NSString *dogName = [self.person valueForKeyPath:@"dog.name"];
NSLog(@"age = %d, %d", self.person.age, age);
NSLog(@"dogName = %@, %@", self.person.dog.name, dogName);
}
@end
打印结果:
age = 18, 18
dogName = 旺财, 旺财
3、setValue: forKey和valueForKey的底层调用顺序:
1)setValue: forKey调用顺序:
先调用setter方法,如果没找到方法,查看accessInstanceVariablesDirectly的返回值,如果是YES,就查找成员变量_key _isKey key isKey 并赋值,如果返回值是NO,直接抛出异常,流程图如下:

ps:accessInstanceVariablesDirectly方法的默认返回值是YES。
2)valueForKey调用顺序:
先调用getter等方法,如果没找到方法,查看accessInstanceVariablesDirectly的返回值,如果是YES,就查找成员变量_key _isKey key isKey 并取值,如果返回值是NO,直接抛出异常,流程图如下:

二、KVO-键值监听:
1、KVO定义:
KVO,Key-Value Observing,键值监听,也叫键值观察,KVO是基于KVC实现的。它是一种观察者模式的衍生,其基本思想是,对目标对象的某个属性添加观察,当该属性发生变化时,通过触发观察者实现的KVO接口方法,来通知观察者。观察者模式较完美将目标对象与观察者对象解耦。
简单来说,KVO可以通过监听key,来获取value的变化,用来在对象之间监听状态的变化。
2、常见的API:
/**
添加观察者
@param observer 观察者,一般是self
@param keyPath 属性或者属性路径
@param options 可选择新值和旧值等,可多选
@param context 上下文,用来区分消息,可以为nil
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
/**
移除观察者
@param observer 观察者,一般是self
@param keyPath 属性或者属性路径
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
如上,先创建一个Person类和Dog类,运行代码:
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property(nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [Person new];
self.person.dog = [Dog new];
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.person addObserver:self forKeyPath:@"dog.name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
self.person.age = 18;
self.person.dog.name = @"旺财";
}
- (void)dealloc {
// 注意移除KVO,否则会造成内存泄漏
[self.person removeObserver:self forKeyPath:@"age"];
[self.person removeObserver:self forKeyPath:@"dog.name"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (object == self.person) {
if ([keyPath isEqualToString:@"age"]) {
NSLog(@"--------KVO-Age------------");
NSNumber *newAge = change[NSKeyValueChangeNewKey];
NSNumber *oldAge = change[NSKeyValueChangeOldKey];
NSLog(@"newAge = %@", newAge);
NSLog(@"oldAge = %@", oldAge);
} else if ([keyPath isEqualToString:@"dog.name"]) {
NSLog(@"--------KVO-DogName--------");
NSString *newDogName = change[NSKeyValueChangeNewKey];
NSString *oldDogName = change[NSKeyValueChangeOldKey];
NSLog(@"newDogName = %@", newDogName);
NSLog(@"oldDogName = %@", oldDogName);
}
}
}
@end
打印结果:
--------KVO-Age------------
newAge = 18
oldAge = 0
--------KVO-DogName--------
newDogName = 旺财
oldDogName = <null>
3、KVO底层实现:
先说结论:使用了KVO,系统通过runtime动态创建一个NSKVONotifying_Person派生类对象,并将person对象的isa指针指向该类,该类是Person的子类,该类重写了setter方法,在setter方法里调用了willChangeValueForKey和didChangeValueForKey,而didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法。
注意:派生类只重写注册了观察者的属性方法。
代码和断点如下:

在打印区调试结果:
// 第一个断点,还没添加KVO,isa指针和class方法均返回Person类对象
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $0 = 0x000000010b7ac3a3 "Person"
(lldb) p [self.person class]
(Class) $0 = Person
// 第二个断点,添加KVO之后,isa指针指向NSKVONotifying_Person对象,class还是返回Person对象
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x0000600000fecac0 "NSKVONotifying_Person"
(lldb) p [NSClassFromString(@"NSKVONotifying_Person") isSubclassOfClass:[Person class]]
(BOOL) $2 = YES
(lldb) p [self.person class]
(Class) $3 = Person
注意:由于派生类是重写了setter方法,所以直接修改成员变量的值不会触发KVO。
KVC会调用setter方法,所以KVC会自动触发KVO。
self.person->_age = 18; //不会触发KVO
self.person.dog.name = @"旺财"; //点调用,即调用setter方法,会触发KVO
[self.person setValue:@"饼哥IT" forKey:@"name"]; //KVC会触发KVO
觉得写的不错,有些启发或帮助,点个赞哦!