OC底层原理二十三:KVO原理
上一节,我们介绍了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
的介绍:
![](https://img.haomeiwen.com/i12857030/4edc201db4026e7a.png)
-
照顾英语不好的同学,我们放上
谷歌翻译
:
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
控制自动
和手动
发送通知
,默认
值为自动
( 👉 官方介绍)
![](https://img.haomeiwen.com/i12857030/05d41db65f63cc4c.png)
- 当我们给
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
,此处一定非常熟悉,这就是RxSwif
t的merge
的原理。) -
相关原理: 👉 官方链接
2.6 数组的观察
-
集合
等类型的监听,与属性的监听不同。我们可以查阅KVC的官方文档:
![](https://img.haomeiwen.com/i12857030/ee275d6a4ef16776.png)
// 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
- 打印结果:
![](https://img.haomeiwen.com/i12857030/8de107c93a2ad397.png)
-
数组
的设值
,必须使用专属API
才可以触发。直接赋值
仅改变数组内部元素
,不会引起
数组地址
的变化
。 -
从打印的结果上,
change
有4种
情况:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
![](https://img.haomeiwen.com/i12857030/0fff0e7c7247ee38.png)
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类
:
![](https://img.haomeiwen.com/i12857030/9e1f702a002a23af.png)
- 观察到
添加观察者
后,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];
- 打印结果:
![](https://img.haomeiwen.com/i12857030/e4d88604bbb759b5.png)
拓展:
- 检验同样
继承
自HTPerosn
的子类HTStudent
,打印结果:// HTStudent @interface HTStudent : HTPerson @end @implementation HTStudent @end
image.png
结论:
- 直接继承的
子类
,没有任何方法
和属性
。可以确定:
KVO派生类
继承自HTPerosn
,重写了setName
、class
、dealloc
方法,新增了_isKVOA
方法
3.2.2 KVO派生类给父类属性赋值
- 在
addObserver
处添加断点,运行代码到此处时,lldb
输入:watchpoint set variable self->_person->_name
:
![](https://img.haomeiwen.com/i12857030/9a177ead07999943.png)
-
设置成功
后,运行代码
,点击屏幕
触发touchesBegan
事件,会进入汇编
页面(观察到设置属性断点处
)
![](https://img.haomeiwen.com/i12857030/839da7f27a45caa7.png)
![](https://img.haomeiwen.com/i12857030/2030db888fcdffaa.png)
- 可以观察到,当
派生类
在调用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。(简化版,重在理解派生类
的流程
和功能
)