上一节,我们介绍了KVC原理,而KVC的工作,绝大部分通过继承NSObject自动处理好了。实际应用中,我们关注得相对较少。而基于KVC的KVO,在应用中却是非常的广泛。
现在我们使用的
响应式框架(RAC、RxSwif、Combine等),实际都是KVO机制的应用。
本节,我们详细讲解KVO:
- KVO介绍
- KVO应用
- KVO原理
引入:
- 我们上一节
分析KVC时,官方对KVC的应用中,第一个介绍的就是KVO:
👉 KVC文档链接
image.png我们点击进入Key-Value Observing Programming Guide (KVO指引)
1. KVO介绍
KVO,全称为Key-value observing键值观察。
-
键值观察是一种机制,允许对象在其他对象的指定属性发生更改时得到通知。
2 KVO应用:
- 测试代码:(
监听person对象的name属性的新值)
// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation HTPerson
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.name = @"ht";
// 1. 添加
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
}
// 2. 监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString: @"name"]) {
NSLog(@"新值:%@", change[NSKeyValueChangeNewKey]);
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@ +",self.person.name];
}
-(void)dealloc {
// 3. 移除
[self.person removeObserver:self forKeyPath:@"name" context: NULL];
}
@end
- 这里为了测试,在
touchesBegan点击事件中添加了name的变更,多次点击,打印结果如下:
image.png
主要步骤: 1. 添加 -> 2. 监听 ->3. 移除
2.1 添加
-
addObserver 添加操作中,addObserver是监听对象,KeyPath是监听路径,option是监听类型,是一个枚举,包含:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew // 新值
NSKeyValueObservingOptionOld // 旧值
NSKeyValueObservingOptionInitial // 初始值
NSKeyValueObservingOptionPrior // 变化前
};
面试官:
添加通知时,context写什么内容?
答:填nil
面试官:回去等通知 😂
- 关于
context的介绍:

-
照顾英语不好的同学,我们放上
谷歌翻译:
image.png context的类型为(void *),所以不能写nil。但可以写成Null。
如果写Null,会默认通过KeyPath路径去确定需要监听的对象。但是这种方法可能导致父类由于不同原因观察到相同的路径,而产生问题。而且查询到父类,消耗的计算资源更多。
所以苹果建议我们可以static void*创建静态的context,这样的好处是:
- 仅从本类中查找当前
context,节省计算资源,更安全;
- 仅从本类中查找当前
-
observeValueForKeyPath监听对象时,我们可以不再通过name去区分当前响应对象。而是使用context精准区分当前响应对象:
image.png
-
-
removeObserver移除对象时,可以通过context精准移除观察对象:
image.png
-
2.2 监听
-
observeValueForKeyPath 监听当前控制器的所有变化,change有以下4种情况:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
-
keyPath、object、context和上述一样。
2.3 移除
一定要移除! 一定要移除! 一定要移除!
网上有很多说
Xcode升级后,不再需要手动移除监听者。仅仅在当前页面操作时,确实不用处理。
- 但如果业务变得复杂,对于
同一对象属性,如果当前页面进行了添加、监听和移除,而其他页面只进行添加和监听,再触发监听时,就会产生KVO Crash。所以我们要养成谁使用谁销毁的习惯。
2.4 开关
-
automaticallyNotifiesObserversForKey控制自动和手动发送通知,默认值为自动( 👉 官方介绍)

- 当我们给
HTPerson添加automaticallyNotifiesObserversForKey方法,返回值为NO后,所有监听消息都不再发送。 - 我们
重写属性setter方法,手动调用API进行消息发送:
@implementation HTPerson
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
-(void)setName:(NSString *)name {
if ([self.name isEqualToString: name]) return; // 值没变化,不操作
[self willChangeValueForKey:@"name"]; // 即将改变
_name = name; // 赋值
[self didChangeValueForKey:@"name"]; // 已改变
}
@end
2.5 路径处理
我们已
downloadProgress下载进度为例,下载进度等于writtenData已下载数据量/totalData总数据量。我们可以聚合
writtenData和totalData两个属性,变成监听downloadProgress一个属性。
// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end
@implementation HTPerson
- (NSString *)downloadProgress {
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [NSString stringWithFormat:@"%.2f", 1.0f * self.writtenData / self.totalData];
}
// 下载进入 writtenData / totalData
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray * affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
// 添加监听
[self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context: NULL];
}
// 处理监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString: @"downloadProgress"]) {
NSLog(@"当前进度:%@", change[NSKeyValueChangeNewKey]);
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.writtenData += 20;
self.person.totalData +=10;
}
-(void)dealloc {
// 移除监听
[self.person removeObserver:self forKeyPath:@"downloadProgress" context: NULL];
}
@end
-
打印结果:
image.png 这里将
writtenData和totalData都变化了,可以看到每次打印的是2条记录。说明聚合的downloadProgress中,只要writtenData或totalData值变化,都会触发一次。
(如果你用过RAC或RxSwift,此处一定非常熟悉,这就是RxSwift的merge的原理。)相关原理: 👉 官方链接
2.6 数组的观察
-
集合等类型的监听,与属性的监听不同。我们可以查阅KVC的官方文档:

// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSMutableArray *dateArray;
@end
@implementation HTPerson
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.name = @"ht ";
self.person.dateArray = [NSMutableArray new];
// 添加监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context: NULL];
}
// 处理监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@: %@", keyPath, change);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
// [self.person.dateArray addObject:@"6"]; //此赋值仅改变数组内部元素,不会引起数组地址的变化
[[self.person mutableArrayValueForKeyPath:@"dateArray"] insertObject:@"6" atIndex:0];
[[self.person mutableArrayValueForKeyPath:@"dateArray"] setObject:@"8" atIndexedSubscript:0];
[[self.person mutableArrayValueForKeyPath:@"dateArray"] removeObjectAtIndex:0];
}
-(void)dealloc {
// 移除监听
[self.person removeObserver:self forKeyPath:@"name" context: NULL];
[self.person removeObserver:self forKeyPath:@"dateArray" context: NULL];
}
@end
- 打印结果:

数组的设值,必须使用专属API才可以触发。直接赋值仅改变数组内部元素,不会引起数组地址的变化。从打印的结果上,
change有4种情况:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};

3. KVO底层原理
3.1 KVO只观察Setter方法
- 我们先观察一个案例,此案例有
public声明的nickName成员变量和@property定义的属性name,分别监听这2个属性值: - 测试代码:
// HTPerson
@interface HTPerson : NSObject
{
@public NSString * nickName;
}
@property (nonatomic, copy) NSString *name;
@end
@implementation HTPerson
@end
// ViewController
@interface ViewController ()
@property (nonatomic, strong) HTPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.name = @"ht ";
// 添加监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
}
// 处理监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@: %@", keyPath, change);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
self.person->nickName = [NSString stringWithFormat:@"%@+", self.person->nickName];
}
-(void)dealloc {
// 移除监听
[self.person removeObserver:self forKeyPath:@"name" context: NULL];
[self.person removeObserver:self forKeyPath:@"nickName" context: NULL];
}
@end
-
打印结果:
image.png 发现只
监听到属性的变化,而监听不到成员变量的变化。而属性和成员变量的区别,核心在于是否实现setter方法。
3.2 KVO派生类
-
在
addObserver处打上断点,运行到断点处后,打印当前person类:
image.png 惊奇的发现,使用
运行时object_getClassName读取的self.person类,是NSKVONotifying_HTPerson类。而使用self.person.class直接打印的类,确是HTPerson类。好像发现了一些不可告人的密码 😃 苹果
金屋藏娇生成了个NSKVONotifying_HTPerson,但又故意的不让外部知道,所以调用class方法,打印的还是HTPerson类。当然,从开发的层面,可以理解,对外事务越简单越好,
高内聚,减轻了开发人员的学习和使用成本。不过对于我们现在探究底层原理而言,就想知道这个NSKVONotifying_HTPerson是什么。
3.2.1 NSKVONotifying_HTPerson与HTPerson什么关系?
- 导入
#import <objc/runtime.h>,添加打印本类和所有子类的方法:
/// 遍历本类及子类
-(void) printClasses: (Class)cls {
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建1个数组
NSMutableArray * mArray = [NSMutableArray arrayWithObject:cls];
//获取所有已注册的类
Class * classes = (Class *)malloc(sizeof(Class) * count);
objc_getClassList(classes, count);
for (int i = 0; i < count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes: %@",mArray);
}
- 在
addObserver前后前后打印self.person类:

- 观察到
添加观察者后,HTPerson多了一个NSKVONotifying_HTPerson子类。
我们添加遍历Ivars、Property、Method的函数:
/// 遍历Ivars
-(void) printIvars: (Class)cls {
// 仿写Ivar结构
typedef struct HT_ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
}HT_ivar_t;
// 记录函数个数
unsigned int count = 0;
// 读取函数列表
Ivar * ivars = class_copyIvarList(cls, &count);
for (int i = 0; i < count; i++) {
HT_ivar_t * ivar = (HT_ivar_t *) ivars[i];
NSLog(@"ivar: %@", [NSString stringWithUTF8String: ivar->name]);
}
free(ivars);
}
/// 遍历属性
-(void) printProperties: (Class)cls {
// 仿写objc_property_t结构
typedef struct Ht_property_t{
const char *name;
const char *attributes;
}Ht_property_t;
// 记录函数个数
unsigned int count = 0;
// 读取函数列表
objc_property_t * props = class_copyPropertyList(cls, &count);
for (int i = 0; i < count; i++) {
Ht_property_t * prop = (Ht_property_t *)props[i];
NSLog(@"property: %@", [NSString stringWithUTF8String:prop->name]);
}
free(props);
}
/// 遍历方法
-(void) printMethodes: (Class)cls {
// 记录函数个数
unsigned int count = 0;
// 读取函数列表
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"method: %@-%p", NSStringFromSelector(sel), imp);
}
free(methodList);
}
- 在
addObserver处添加打印代码,分别检查HTperson本类和NSKVONotifying_HTPerson派生类的Ivars、Property和Method:
// 添加监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context: NULL];
NSLog(@"------- NSKVONotifying_HTPerson --------");
[self printMethodes: objc_getClass("NSKVONotifying_HTPerson")];
[self printIvars: objc_getClass("NSKVONotifying_HTPerson")];
[self printProperties: objc_getClass("NSKVONotifying_HTPerson")];
NSLog(@"------- HTPerson --------");
[self printMethodes: HTPerson.class];
[self printIvars: HTPerson.class];
[self printProperties: HTPerson.class];
- 打印结果:

拓展:
- 检验同样
继承自HTPerosn的子类HTStudent,打印结果:// HTStudent @interface HTStudent : HTPerson @end @implementation HTStudent @endimage.png
结论:
- 直接继承的
子类,没有任何方法和属性。可以确定:
KVO派生类继承自HTPerosn,重写了setName、class、dealloc方法,新增了_isKVOA方法
3.2.2 KVO派生类给父类属性赋值
- 在
addObserver处添加断点,运行代码到此处时,lldb输入:watchpoint set variable self->_person->_name:

-
设置成功后,运行代码,点击屏幕触发touchesBegan事件,会进入汇编页面(观察到设置属性断点处)


- 可以观察到,当
派生类在调用willChange和didChange中间,调用了[HTPerson setName]方法,完成了给父类HTPerson的name属性赋值。(此时的willChange和didChange方法是继承自NSObject的)
3.2.3 KVO派生类何时移除,是否真移除?
-
我们在
removeObserver处加入断点,分别在removeObserver前后使用object_getClassName()打印当前isa指向的类:
image.png 发现
NSKVONotifying_HTPerson在外部removeObserver时,完成的移除操作。将isa指回了原类。但是我们在
removeObserver移除操作之后,打印HTPerosn类和子类的信息,发现NSKVONotifying_HTPerson派生类并没有移除。
ps:
页面销毁之后再打印HTPerosn类和子类,也一样存在NSKVONotifying_HTPerson派生类。
KVO派生类只要生成,就会一直存在,这样可以减少频繁的添加操作
至此,我们已经知道KVO是创建派生类实现了键值观察。
- 添加:
addObserver时,创建了派生类,派生类是当前类的子类,重写了被监听属性的setter方法,并将当前类的isa指向了派生类。
(此时开始,所有调用本类的方法,都是调用的派生类。派生类中没有的方法,就会沿着继承链查询到本类)
- 添加:
- 赋值:
派生类重写了被监听属性的setter方法,在派生类的setter方法触发时:在willChange之后,didChange之前,调用父类属性settter方法,完成父类属性的赋值`。
- 赋值:
- 移除: 在
removeObserver后,isa从派生类指回本类。 但创建过的派生类,不会被本类从子类列表中移除,会一直存在。
- 移除: 在
- 假象: 之所以外部
打印class永远看不到派生类,是因为派生类将class方法重写了,故意不让外界看到。
(知道越多,烦恼越多 😂 ,就让派生类做个默默付出的无名英雄吧)
- 假象: 之所以外部
下一节,我们纯代码自定义KVO。(简化版,重在理解派生类的流程和功能)









