1. KVO
KVO
,(Key-Value Observing)
,即键值观察
,是一种机制,允许注册成为其他对象的观察者,当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知。
在日常开发中,我们使用 KVO
来监听对象属性的变化,并做出响应,现在我们来看下KVO
的底层实现。
2. KVO
的基本使用
基本使用可以分为以下 3 步:
1.注册观察者
addObserver:forKeyPath:options:context
一般最后的context
属性,一般用来区分回调来源,当然也可以使用keyPath
2.实现 KVO 回调
observeValueForKeyPath:ofObject:change:context
3.移除观察者
removeObserver:forKeyPath:context
移除观察者调用方法是必须的,和注册观察者方法成对出现。
2.1 案例一:基本使用
首先有一个Person
类,只有一个name
属性,代码如下:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
ViewController 中使用:
@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 *)touches withEvent:(UIEvent *)event{
_person.name = @"哈哈";
}
// 响应方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)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 - - {
kind = 1;
new = "\U54c8\U54c8";
old = "";
}
其中的 kind
表示键值变化的类型,是一个枚举:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//设值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替换
};
2.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 *)touches withEvent:(UIEvent *)event{
// name 的值即将改变
[_person willChangeValueForKey:@"name"];
_person.name = @"哈哈";
// name的值改变完成
[_person didChangeValueForKey:@"name"];
}
2.3 案例三 观察属性.属性的变化
我们再创建一个Dog
类,并且Person
类中也添加一个Dog
对象:
@interface Dog : NSObject
@property(nonatomic,assign)int age;
@end
#import "Dog.h"
@implementation Dog
@end
我们想要观察 person.dog
的 age
的属性变化,此时需要修改添加观察者的代码,keyPath
改成dog.age
即可:
[_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
2.4 案例四 注册一个KVO
观察者,可以监听多个属性的变化
我们在 Person
类中再添加两个属性:firstName
和 lastName
,它们两个是name
的组成,当给name
添加通知的时候,当 firstName
和 lastName
任意一个变化的时候,都要收到通知:
- 在
Person
中加入类方法+keyPathsForValuesAffectingValueForKey
,返回一个容器:
+ (NSSet *)keyPathsForValuesAffectingName{
NSSet *keyPaths = [NSSet setWithArray:@[@"firstName",@"lastName"]];
return keyPaths;
}
注册观察person
的 name
属性,这样就可以在 firstName
和 lastName
变化时,也收到相应的回调了。
2.5 案例五 可变数组
我们给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 *)touches withEvent:(UIEvent *)event{
// sons添加元素
[_person.sons addObject:@"lili"];
}
直接通过[_person.sons addObject:@"lili"];
这样是无法触发KVO 回调
的,针对于可变数组的集合类型,需要通过mutableArrayValueForKey
方法将元素添加到可变数组中,才能触发 KVO
回调,将添加元素的代码改为如下代码即可:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[[_person mutableArrayValueForKey:@"sons"] addObject:@"lili"];
}
输出如下,可以看到收到相应的回调了:
2021-01-18 16:35:05.850405+0800 001-alloc&init探索[3865:311198] sons - - {
indexes = "<_NSCachedIndexSet: 0x600000041020>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
lili
);
}
3. KVO 底层探索
在 KVO
的官方文档中,有如下说明:
-
KVO
是通过isa-swizzling
的技术实现的 - 当为对象的属性添加观察者时,会修改观察对象的
isa
指针,指向一个中间类,此时isa
指针的值不一定反映对象的实际类 - 不应依靠
isa
指针来确定类成员身份,应该使用class
方法来确定对象实例的类
3.1 验证 KVO 只对属性观察
我们给 Person
添加一个成员变量name
,一个属性 nickName
,分别注册KVO
观察,当值发生变化时,是否都能收到回调?
////// Person类:
@interface Person : NSObject
{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
@implementation Person
@end
////// ViewController中:
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [Person alloc];
[_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[_person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
_person.nickName = @"嘻嘻";
_person->name = @"哈哈";
}
// 响应方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"%@ - %@ - %@",keyPath,object,change);
}
// 移除观察者
- (void)dealloc{
[_person removeObserver:self forKeyPath:@"nickName"];
[_person removeObserver:self forKeyPath:@"name"];
}
@end
运行结果:
2021-01-18 16:45:19.627398+0800 KVO[4055:350986] nickName - - {
kind = 1;
new = "\U563b\U563b";
old = "";
}
可以看到,只收到了 nickName
变化的回调,所以KVO
只对属性进行观察,观察的是 setter
方法。
3.2 验证中间类
根据官方文档描述,在注册KVO
观察者后,观察对象的isa 指针
会发生改变,指向了一个中间类
。
-
注册观察者之前,实例对象
person
的isa 指针
指向Person
:
-
注册观察者之后,
person
的isa
指针指向了NSKVONotifying_Person
:
在注册观察者后,实例对象的isa
指针指向由Person
类变为了NSKVONotifying_Person
中间类,即实例对象的isa指针
指向确实发生了变化。
那么这个NSKVONotifying_Person和 Person 类有什么关系?
我们获取
NSKVONotifying_Person
类的父类,可以看到是Person
,NSKVONotifying_Person
是Person
的子类。
生成这个中间类肯定是有他的作用,我们去看下这个类有什么内容,可以通过下面的方法获取NSKVONotifying_Person
类中所有的方法:
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i
输出结果如下:
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
从结果中可以看出有 4 个方法:setNickName
、class
、dealloc
、_isKVOA
,那么这些方法是继承的还是重写的呢?
我们也输出一下 Person
类的方法列表,和NSKVONotifying_Person
的方法列表进行对比:
可以看到方法地址都不同,说明NSKVONotifying_Person
重写了父类 Person
的 setNickName
方法,同时重写了基类NSObject
的 class
、dealloc
、_isKVOA
方法。
那么当移除观察者后,被观察对象的 isa
又指向了谁?NSKVONotifying_Person
这个中间类还会存在吗?
-
直接断点查看
可以看到,在移除 KVO
观察者之后,isa
的指向又从NSKVONotifying_Person
变成了 Person
,那么现在中间类是否还存在么?
我们通过打印 Person
的子类情况,判断中间类是否销毁:
通过打印结果可以看出,中间类没有被销毁
,还在内存中,主要是为了重用
。
3.3 总结
- 实例对象的
isa
指向在注册KVO
观察者之后,由原有类更改为指向中间类,类名为NSKVONotifying_原有类名
- 中间类重写了被观察属性的
setter 方法
、class
、dealloc
、_isKVO
方法 -
dealloc
方法中,移除KVO
观察者之后,实例对象isa
指向由中间类改为原有类 - 中间类在移除观察者后也并不会被销毁
4. 自定义 KVO
既然知道了系统KVO
的大概实现,我们可以模仿一下,并进行一些优化处理:
- 将注册和响应通过函数式编程,即
block
的方法结合在一起 - 去掉系统繁琐的三部曲,实现
KVO
自动销毁机制
在系统中,注册观察者和 KVO
相应属于响应式编程
,就是分开写的,为了使代码更集中,将注册和回调的逻辑组合在一起,即采用函数式编程
方式,分为三部分:
- 注册观察者
//*********定义block*********
typedef void(^KVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
//*********注册观察者*********
- (void)tt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(KVOBlock)block;
KVO
响应
这部分主要是重写setter
方法,在中间类的setter
方法中,通过block
的方式传递给外部进行响应移除观察者
//*********移除观察者*********
- (void)tt_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
之后我们创建一个NSObject
的分类,实现上面三个方法。
4.1 注册观察者
注册观察者方法中,主要有以下几部分操作:
- 1.判断当前观察
keyPath
的setter
方法是否存在
// 验证是否有 setter 方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
Class class = object_getClass(self);
SEL setterSEL = NSSelectorFromString([self setterForGetter:keyPath]);
Method setterMethod = class_getInstanceMethod(class, setterSEL);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"KVO - 没有当前%@的setter方法", keyPath] userInfo:nil];
}
}
// getter 转 setter
- (NSString*)setterForGetter:(NSString*)key{
if (key.length <= 0) {
return nil;
}
key = [key capitalizedStringWithLocale:[NSLocale currentLocale]];
NSString *setter = [NSString stringWithFormat:@"set%@:",key];
return setter;
}
- 2.动态生成子类,将需要重写的
class
方法添加到中间类中
// 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 原有类名
NSString *oriClassName = NSStringFromClass([self class]);
// 新的子类名
NSString *childClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",oriClassName];
// 获取子类
Class childCls = NSClassFromString(childClassName);
if(childCls){
return childCls;
}
// 申请类
childCls = objc_allocateClassPair([self class], childClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(childCls);
// 添加方法
SEL classSel = @selector(class);
Method classMethod = class_getInstanceMethod([self class], classSel);
const char *classType = method_getTypeEncoding(classMethod);
class_addMethod(childCls, classSel, (IMP)tt_class, classType);
return childCls;
}
// 重写class方法,为了与系统类对外保持一致
Class tt_class(id self, SEL _cmd){
//在外界调用class返回Person类
return class_getSuperclass(object_getClass(self));
}
- 3.将被观察实例对象的
isa
指向由原有类改为中间类
objc_setClass(self,childCls);
- 4.保存信息,这里用的是数组,需要创建保存信息的模型类
@interface KVOInfo : NSObject
@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) KVOBlock handleBlock;
- (instancetype)initWithObserver:(id)observer forKeyPath:(NSString *)keyPath handleBlock:(KVOBlock)block;
@end
@implementation KVOInfo
- (instancetype)initWithObserver:(id)observer forKeyPath:(NSString *)keyPath handleBlock:(KVOBlock)block{
self = [super init];
if (self) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
///////// 保存信息
//- 保存多个信息
KVOInfo *info = [[KVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
//使用数组存储 -- 也可以使用map
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey));
if (!mArray) {//如果mArray不存在,则重新创建
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
完整的注册观察者代码如下:
- (void)tt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(KVOBlock)block{
// 1.验证 setter 方法是否存在
[self judgeSetterMethodFromKeyPath:keyPath];
// 2.保存信息
KVOInfo *info = [[KVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey));
if (!mArray) {//如果mArray不存在,则重新创建
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
// 3.动态生成子类
Class childCls = [self createChildClassWithKeyPath:keyPath];
// 4.更改 isa 指向
object_setClass(self, childCls);
// 5.添加一个 setter 方法
SEL setterSel = NSSelectorFromString([self setterForGetter:keyPath]);
//获取setter实例方法
Method method = class_getInstanceMethod([self class], setterSel);
//方法签名
const char *type = method_getTypeEncoding(method);
//添加一个setter方法
class_addMethod(childCls, setterSel, (IMP)tt_setter, type);
}
生成的中间类的 class 方法必须重写,其目的是为了和系统一样,对外获取的类保持一致
。
tt_setter
方法实现,会在下面进行讲述。
4.2 KVO 响应
主要是给子类添加 setter 方法,其目的是为了在setter
方法中向父类发送消息,告知其属性值的变化:
static void tt_setter(id self, SEL _cmd, id newValue){
NSLog(@"来了:%@",newValue);
//此时应该有willChange的代码
//往父类Person发消息 - 通过objc_msgSendSuper
//通过系统强制类型转换自定义objc_msgSendSuper
void (*tt_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
//定义一个结构体
struct objc_super superStruct = {
.receiver = self, //消息接收者 为 当前的self
.super_class = class_getSuperclass(object_getClass(self)), //第一次快捷查找的类 为 父类
};
//调用自定义的发送消息函数
tt_msgSendSuper(&superStruct, _cmd, newValue);
//此时应该有didChange的代码
//让vc去响应
/*---函数式编程*/
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey));
for (KVOInfo *info in mArray) {
NSMutableDictionary *change = [NSMutableDictionary dictionaryWithCapacity:1];
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
4.3 移除观察者
为了避免在外界不断的调用removeObserver
方法,在自定义KVO
中实现自动移除观察者。
实现tt_removeObserver:forKeyPath:
方法,主要是清空数组,以及isa指向
更改
- (void)tt_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey));
if (mArray.count <= 0) {
return;
}
for (KVOInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath]) {
[mArray removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
if (mArray.count <= 0) {
//isa指回父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
在子类中重写dealloc
方法,当子类销毁时,会自动调用dealloc
方法(在动态生成子类的方法中添加)
#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
//...
//添加dealloc 方法
SEL deallocSel = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
const char *deallocType = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSel, (IMP)tt_dealloc, deallocType);
return newClass;
}
//************重写dealloc方法*************
void tt_dealloc(id self, SEL _cmd){
NSLog(@"来了");
Class superClass = [self class];
object_setClass(self, superClass);
}
其原理主要是Person
释放即调用 dealloc
了,就会自动走到重写的 tt_dealloc
方法中(原因是person
对象的isa
目前还在指向中间类,但是实例对象的地址是不变的,所以 person
释放时,会调用子类重写 tt_dealloc
方法),达到自动移除观察者的目的。
这个例子是为了能够更方便的理解KVO
底层原理,仅做参考。