1.定义
KVO (Key-Value Observing) 键值观察,是iOS中,对象采用的一种非正式协议,用于通知其他对象的指定属性的更改。
KVO是通过NSObject的一个分类NSKeyValueObserving来实现的,所以所有继承自NSObject的类都可以使用KVO,不是继承自NSObject就不具备KVO的功能(如结构体或是一些纯swift的类)
苹果文档的定义是:An informal protocol that objects adopt to be notified of changes to the specified properties of other objects. 如下图:
鼠标放在NSObject类名上,点击option会弹出文档
2.主要方法,和代码示例
注意点
KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。
添加观察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
实现属性值改变时会调用的方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
示例代码:
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (strong, nonatomic) IBOutlet UILabel *label;
@property(strong,nonatomic)Person *person;
@end
@implementation ViewController
- (IBAction)btnClick:(id)sender {
self.person.name = @"newName";
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person = [Person new];
//NSKeyValueObservingOptions
//NSKeyValueObservingOptionNew:提供更改前的值
//NSKeyValueObservingOptionOld:提供更改后的值
//NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
//NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(即一次修改有两次触发)
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:@"这是什么"];
self.person.name = @"name";
self.label.text = self.person.name;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",keyPath);
NSLog(@"%@", change);
NSLog(@"%@",object);
NSLog(@"%@",context);
NSLog(@"-----------------------------------");
self.label.text = [change objectForKey:@"new"];
}
- (void)dealloc{
NSLog(@"dealloc");
[self.person removeObserver:self forKeyPath:@"name"];
}
@end
打印结果:
2020-07-16 14:39:15.922700+0800 KVO[2852:79289] name
2020-07-16 14:39:15.922924+0800 KVO[2852:79289] {
kind = 1;
new = name;
old = “”;
}
2020-07-16 14:39:15.923065+0800 KVO[2852:79289] <Person: 0x600001360180>
2020-07-16 14:39:15.923190+0800 KVO[2852:79289] 这是什么
2020-07-16 14:39:15.923296+0800 KVO[2852:79289] -----------------------------------
2020-07-16 14:39:18.521655+0800 KVO[2852:79289] name
2020-07-16 14:39:18.521934+0800 KVO[2852:79289] {
kind = 1;
new = newName;
old = name;
}
2020-07-16 14:39:18.522121+0800 KVO[2852:79289] <Person: 0x600001360180>
2020-07-16 14:39:18.522285+0800 KVO[2852:79289] 这是什么
2020-07-16 14:39:18.522424+0800 KVO[2852:79289] -----------------------------------
3.KVO的底层实现原理(isa-swizzling)
如下以Person类为例,Person的实例对象使用KVO,调用addObserver方法后:
1.动态生成了中间类NSKVONotifying_Person, 且NSKVONotifying_Person的父类是Person
2.改写了中间类NSKVONotifying_Person的class方法,使调用class方法的时候返回Person
3.重写NSKVONotifying_Person类name属性的setter方法,使得name值改变时,观察者可以收到通知,observeValueForKeyPath方法被调用
4.重写NSKVONotifying_Person类 dealloc 方法来释放资源
5.重写NSKVONotifying_Person类的_isKVOA方法,用来标示该类是一个 KVO 机制声称的类
self.person = [Person new];
NSLog(@"realClass:%@",object_getClass(self.person));
NSLog(@"class:%@",[Person class]);
NSLog(@"realSuperClass:%@",class_getSuperclass(object_getClass(self.person)));
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
NSLog(@"addObserver后");
NSLog(@"realClass:%@",object_getClass(self.person));
NSLog(@"class:%@",[Person class]);
NSLog(@"realSuperClass:%@",class_getSuperclass(object_getClass(self.person)));
打印结果:
2020-07-16 20:20:31.596967+0800 KVO[5621:195863] realClass:Person
2020-07-16 20:20:31.597122+0800 KVO[5621:195863] class:Person
2020-07-16 20:20:31.597277+0800 KVO[5621:195863] realSuperClass:NSObject
2020-07-16 20:20:31.597678+0800 KVO[5621:195863] addObserver后
2020-07-16 20:20:31.597785+0800 KVO[5621:195863] realClass:NSKVONotifying_Person
2020-07-16 20:20:31.597913+0800 KVO[5621:195863] class:Person
2020-07-16 20:20:31.598015+0800 KVO[5621:195863] realSuperClass:Person
从上面代码的打印可以看出,调用addObserver方法,使用KVO的时候,系统生成了一个中间类NSKVONotifying_Person,而且这个中间类继承自Person.
当自己手动添加一个NSKVONotifying_Person类时,会导致KVO失效,报:
[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class
如下图:
如果在observeValueForKeyPath方法里打断点,沿着断点一步步向前调试,如下图:
会发现从name值改变的代码到observeValueForKeyPath方法被调用,经历了如下方法
猜测NSKVONotifying_Person类,setName方法的实现可能类似下面的oc代码:
- (void)setName:(NSString *)name{
_NSSetObjectValueAndNotify();
}
void _NSSetObjectValueAndNotify {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
- (void)didChangeValueForKey:(NSString *)key{
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
用runtime,打印一下Person和NSKVONotifying_Person的方法
self.person = [Person new];
[self getMethod];
NSLog(@"--------------");
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
[self getMethod];
- (void)getMethod {
unsigned int count;
Method *methods = class_copyMethodList(object_getClass(self.person), &count);
for (NSInteger index = 0; index < count; index++) {
Method method = methods[index];
NSString *methodStr = NSStringFromSelector(method_getName(method));
NSLog(@"%@\n", methodStr);
}
}
打印结果
2020-07-17 17:44:03.989443+0800 KVO[4100:137298] .cxx_destruct
2020-07-17 17:44:03.989644+0800 KVO[4100:137298] name
2020-07-17 17:44:03.989781+0800 KVO[4100:137298] setName:
2020-07-17 17:44:03.989902+0800 KVO[4100:137298] --------------
2020-07-17 17:44:08.158801+0800 KVO[4100:137298] setName:
2020-07-17 17:44:08.158995+0800 KVO[4100:137298] class
2020-07-17 17:44:08.159136+0800 KVO[4100:137298] dealloc
2020-07-17 17:44:08.159311+0800 KVO[4100:137298] _isKVOA
发现NSKVONotifying_Person重写了4个方法,重写这些方法的作用:
setName:在属性改变的时候通知观察者,调用observeValueForKeyPath方法
class:重写这个方法,是为了伪装苹果自动为我们生成的中间类。
dealloc:应该是处理对象销毁之前的一些收尾工作
_isKVOA:告诉系统使用了kvo
4.动态生成一个类,仿写KVO
自己给NSObject写一个kvo的分类
NSObject+kvo.h
@interface NSObject (kvo)
- (void)xy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NSObject+kvo.m
#import "NSObject+kvo.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation NSObject (kvo)
- (void)xy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//动态添加一个类
NSString *originClassName = NSStringFromClass([self class]);
NSString *newClassName = [@"XYKVO_" stringByAppendingString:originClassName];
const char *newName = [newClassName UTF8String];
//动态添加的类为当前类的子类
Class kvoClass = objc_allocateClassPair([self class], newName, 0);
//添加setter方法
class_addMethod(kvoClass, @selector(setName:), (IMP)setName, "v@:@");
//注册新添加的这个类
objc_registerClassPair(kvoClass);
//修改isa指针,由xx指向XYKVO_xx
object_setClass(self,kvoClass);
//保存观察者到当前实例对象中
objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark - 重写父类方法
//给动态的类添加的setter方法
void setName(id self, SEL _cmd, NSString *name) {
//保存动态生成的KVO类
Class kvoClass = [self class];
//将isa指针指回父类xx
object_setClass(self, class_getSuperclass([self class]));
//调用父类的setter方法
objc_msgSend(self, @selector(setName:), name);
//取出实例对象的观察者
id objc = objc_getAssociatedObject(self, (__bridge const void *)@"observer");
//通知观察者,执行通知方法
objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:),name,self,nil,name);
//重新修改为XYKVO_xx类
object_setClass(self, kvoClass);
}
@end
使用自己写的kvo分类
参考博客
iOS KVC和KVO详解
KVO原理分析及使用进阶(FBKVOController)
分析实现-实现KVO(YYModel)