iOS 底层原理 | KVO
一、KVO 概述
KVO,(Key-Value Observing),即键值监听,是一种机制,允许注册成为其他对象的观察者,当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知。
二、KVO 基本使用
必须执行以下步骤,才能使对象接收
KVO
兼容属性通知的键值:
- 将观察者注册到观察对象上 使用这个方法:
addObserver:forKeyPath:options:context:
。- 实现
observeValueForKeyPath:ofObject:change:context:
来接收观察者内部值的变化的通知消息。- 在观察者从内存释放之前,调用
removeObserver:forKeyPath:
来移除观察者。
- 1. 注册观察者
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
- 2. 注册观察者
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
- 3. 注销观察者
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
1. 基本使用
首先有一个 Person
类,只有一个 name
属性,代码如下:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
ViewController.m
中:
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [Person alloc];
// 注册 self 也就是 controller 为自己的观察者
[_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_person.name = @"哈哈";
}
// 响应方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@ - %@ - %@",keyPath,object,change);
}
// 移除观察者
- (void)dealloc{
[_person removeObserver:self forKeyPath:@"name"];
}
@end
点击屏幕,控制台输出:
2021-01-18 15:59:15.571749+0800 KVO[3322:245856] name - <Person: 0x6000008c0350> - {
kind = 1;
new = "\U54c8\U54c8";
old = "<null>";
}
NSKeyValueChange
值:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//设值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替换
};
2. 手动触发 observer 回调
我们改变了 name
的值,就自动触发了 observer
回调,但是有时候我们并不一定每一次都通知,比如满足某一个条件的时候,才想着通知一下。
我们先在 Person
中添加一个方法,关闭自动触发:
// 对所有的都关闭
//+ (BOOL)automaticallyNotifiesObserversOfName {
// return NO;
//}
// 可以对某个指定key 关闭
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if([key isEqualToString:@"name"]){
NSLog(@"关闭了自动触发");
return NO;
}
return YES;
}
在 person.name
赋值的地方添加上手动触发的代码:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// name 的值即将改变
[_person willChangeValueForKey:@"name"];
_person.name = @"哈哈";
// name的值改变完成
[_person didChangeValueForKey:@"name"];
}
3. 观察属性.属性的变化
Person
类中有 Dog
属性,Dog
类中有 age
属性,观察 age
的变化如下:
[_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
4. 观察多个属性变化
给 Person
类中再添加两个属性:firstName
和 lastName
,它们两个是 name
的组成,当给 name
添加监听时,当 firstName
和 lastName
任意一个变化,都要收到通知:
- 在
Person
类中加入类方法+ keyPathsForValuesAffectingValueForKey
,返回一个容器。 - 注册观察
Person
的name
属性,这样就可以在firstName
和lastName
变化时,也收到相应的回调。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingName {
NSSet *keyPaths = [NSSet setWithArray:@[@"firstName",@"lastName"]];
return keyPaths;
}
4. 观察可变数组变化
Person
添加一个可变数组属性,当向这个可变数组添加数据时,是不会调用 setter
方法的,也不会触发 KVO
的回调,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
_person = [Person alloc];
// sons是一个可变数组
_person.sons = [@[] mutableCopy];
// 观察 sons 的变化
[_person addObserver:self forKeyPath:@"sons" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// sons添加元素
[_person.sons addObject:@"lili"];
}
直接通过 [_person.sons addObject:@"lili"];
无法触发 KVO
回调,针对于可变数组的集合类型,需要通过 mutableArrayValueForKey
方法将元素添加到可变数组中,才能触发 KVO
回调,将添加元素的代码改为如下代码即可:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[[_person mutableArrayValueForKey:@"sons"] addObject:@"lili"];
}
三、KVO 底层探索
KVO
使用isa-swizzling
技术实现自动键值观察。
- 该isa指针,指向对象的类,它保持一个调度表。该分派表实质上包含指向该类实现的方法的指针以及其他数据。
- 当为对象的属性注册观察者时,将修改观察对象的 isa 指针,指向中间类而不是真实类。结果,isa 指针的值不一定反映实例的实际类。
- 您永远不要依靠 isa 指针来确定类成员。相反,您应该使用该
[ class]
方法确定对象实例的类。
1. 验证 KVO 只对属性观察
- 随意创建一个 Project ,创建一个类,类拥有一个属性,一个成员变量。
- 实例化一个类的对象,并添加属性和成员变量的观察者都为
ViewController
。 - 添加
touchBegin
方法,做到点击屏幕,就让属性和实例变量都发生变化。 KVO
观察的是拥有setter
方法的变量,可以是成员变量,但是必须使用KVC
进行赋值,属性则是可以直接赋值进行观察的。
(注:具体验证自行实现)
2. 验证 KVO 中间类
根据官方文档描述,在注册 KVO
观察者后,观察对象的 isa
指针会发生改变,指向了一个中间类。
注册前后类名对比:
获取NSKVONotifying_Person类的父类,可以看到是 Person
,NSKVONotifying_Person
是 Person
的子类:
我们也可以通过以下代码获取类信息:
- (void)printClassAllMethod:(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(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
///// 调用
[self printClassAllMethod:objc_getClass("NSKVONotifying_Person")];
得:
2021-01-18 20:47:41.791402+0800 KVO[4396:407755] setNickName:-0x10918c54b
2021-01-18 20:47:41.791516+0800 KVO[4396:407755] class-0x10918afd5
2021-01-18 20:47:41.791612+0800 KVO[4396:407755] dealloc-0x10918ad3a
2021-01-18 20:47:41.791706+0800 KVO[4396:407755] _isKVOA-0x10918ad32
NSKVONotifying_Person
类拥有着4个方法:
setXxx
: 重写被观察的属性的set
方法。class
: 重写自己的class
方法。dealloc
: 重写自己的dealloc
方法。_isKVOA
: 判断是不是KVO
生成的中间类。
在进行了 KVO
观察之后的对象,它的 isa
再指向的就是 NSKVONotifying_xxx
这个类,做的改变都会找到 NSKVONotifying_xxx
的 set
方法来对属性进行更改。
NSKVONotifying_xxx
这个中间子类会重写父类的 setter
、dealloc
、 class
方法。并且当观察者注销后,中间类不会被销毁,而是缓存起来,有需要的时候直接调用,减少了下次再添加观察者的时候的开支。