一、KVO基本用法
//例如有一个Person,里面有个属性age
self.person = [[Peerson alloc]init];
self.person.age = 10;
//给person对象添加一个kvo监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
//给当前的控制器添加一个person的监听对象用用来监听age
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.age = 20;//当屏幕点击的时候,改变page
}
//当监听到age发生改变的时候,会调用下面这个方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
// object 就是self.person对象
// keyPath就是 age
// change 就是 {"old":10,"new":20,"kind":1}
}
//注意移除
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"age"];
}
二、KVO本质分析
1.为何监听后会变化
self.person1 = [[Peerson alloc]init];
self.person1.age = 1;
self.person2 = [[Peerson alloc]init];
self.person2.age = 2;
//给person1添加一个监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// self.person1.age = 20;
// self.person2.age = 30;
[self.person1 setAge:20];
[self.person2 setAge:30];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值发生了改变%@", object, keyPath, change);
}
/*
打印:
监听到<MJPerson: 0x600000a388f0>的age属性值发生了改变{
kind = 1;
new = 20;
old = 1;
}
*/
为什么上面代码能监听到person1的改变而监听不到person2改变????
2.分析发生变化
此时在上述代码中打印一下person1的isa和person2的isa
/*
p self.person1.isa
(Class) $0 = NSKVONotifying_MJPerson
Fix-it applied, fixed expression was:
self.person1->isa
------------------------
p self.person2.isa
(Class) $1 = MJPerson
Fix-it applied, fixed expression was:
self.person2->isa
*/
我们会发现 self.person1.isa = NSKVONotifying_MJPerson,
self.person2.isa = MJPerson
我们知道,实例对象指针指向的位置,就是类对象,说明self.person1的类对象是NSKVONotifying_MJPerson已经不是原来的MJPerson
NSKVONotifying_MJPerson这个类对象其实,是runtime动态创建出来的,并且它是继承MJPerson类的
用关系图表示:
而NSKVONotifying_MJPerson类对象中,肯定有isa,superclass,setAge(其它的暂时不用看)
当调用self.person1.age = 20的时候,
- 会先创建NSKVONotifying_MJPerson类继承MJPerson
- 里面会有一个setAge方法,
- setAge方法中调用了Foundation中的_NSSetIntValueNotify方法
大概用伪代码来实现一下
// ----------------NSKVONotifying_MJPerson.h文件-----------------
#import "MJPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSKVONotifying_MJPerson : MJPerson
@end
NS_ASSUME_NONNULL_END
// ----------------NSKVONotifying_MJPerson.m文件-----------------
#import "NSKVONotifying_MJPerson.h"
@implementation NSKVONotifying_MJPerson
- (void)setAge:(int)age{
//肯定会调用Foundation中的这个方法
_NSSetIntValueNotify();
}
//因为Foundation代码没有开源,只能猜测NSSetIntValueNotify方法的实现
void NSSetIntValueNotify(){
[self willChangeValueForKey:@"age"];
[super setAge:age];//调用原来的set方法
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
//通知监听器,某某属性发生了改变
[observer observerValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
而self.person2的指针指向MJPerson类对象,直接从MJPerson类对象中找到setAge方法实现就可以了.
用图看一下就是这样:
三、KVO本质的验证
看看添加之前和添加之后,它们的类对象有哪些变化
#import <objc/runtime.h>
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person1 = [[MJPerson alloc]init];
self.person1.age = 1;
self.person1.height = 11;
self.person2 = [[MJPerson alloc]init];
self.person2.age = 2;
self.person2.height = 22;
//
NSLog(@"person1添加kvo监听之前类对象 %@ - %@", object_getClass(self.person1),
object_getClass(self.person2));
//给person1的age添加一个监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person1添加kvo监听之后类对象 %@ - %@", object_getClass(self.person1),
object_getClass(self.person2));
/*
打印:
1651740686.076586 person1添加kvo监听之前类对象 MJPerson - MJPerson
1651740686.076676 person1添加kvo监听之后类对象 NSKVONotifying_MJPerson - MJPerson
*/
}
添加之前类对象是MJPerson,执行的是MJPerson中setAge方法
添加以后person1类对象是NSKVONotifying_MJPerson,那么究竟执行的NSKVONotifying_MJPerson中哪个方法
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person1 = [[MJPerson alloc]init];
self.person1.age = 1;
self.person1.height = 11;
self.person2 = [[MJPerson alloc]init];
self.person2.age = 2;
self.person2.height = 22;
//methodForSelector这个可以看到执行方法的内存地址
NSLog(@"person1添加kvo监听之前执行的方法 %p -%p", [self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
//给person1的age添加一个监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person1添加kvo监听之后执行的方法 %p -%p", [self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
/*
打印:
1651741040.976810 person1添加kvo监听之前执行的方法 0x1061d8da0 -0x1061d8da0
1651741040.976951 person1添加kvo监听之后执行的方法 0x7fff207b1cfb -0x1061d8da0
*/
}
可以看到:添加kvo之前person1和person2都是执行的同一个方法0x1061d8da0,
添加kvo以后person1执行的是0x7fff207b1cfb这个方法
那么如何查看0x1061d8da0对应的具体方法???
通过p (IMP)0x1061d8da0转换,如图
NSKVONotifying_MJPerson的isa指针指向自己的元类,meta-NSKVONotifying_MJPerson
四、窥探KVO本质执行顺序
至于判断Foundation中有没有NSSetIntValueAndNotify方法,只能通过反编译查看。这里不做详细介绍
前面提到说_NSSetIntValueNotify中先调用willChangeValueForKey在调用setAge:age在调用didChangeValueForKey,然后didChangeValueForKey中调用observerValueForKeyPath:key,方法进行验证一下
- (void)setAge:(int)age{
//肯定会调用Foundation中的这个方法
_NSSetIntValueNotify();
}
//因为Foundation代码没有开源,只能猜测NSSetIntValueNotify方法的实现
void NSSetIntValueNotify(){
[self willChangeValueForKey:@"age"];
[super setAge:age];//调用原来的set方法
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
//通知监听器,某某属性发生了改变
[observer observerValueForKeyPath:key ofObject:self change:nil context:nil];
}
我们在person中重新写 willChangeValueForKey这些方法,看看执行顺序如图
五、获取NSKVONotifying_MJPerson中类方法
- (void)printMethodNamesOfClass:(Class)cls{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
}
/*
打印:
Interview01[17722:182442] NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
Interview01[17722:182442] MJPerson age, setAge:,
*/
从打印结果就可以看出NSKVONotifying_MJPerson中包含了setAge:, class, dealloc, _isKVOA这些方法
如果给peson1添加kvo那么它的指针其实已经指向了NSKVONotifying_MJPerson,调用的方法也是NSKVONotifying_MJPerson中的setAge
class:重写了父类的class,当你用[self.person1 class]获取类对象的时候还是MJPerson,而不是 NSKVONotifying_MJPerson,为了就是让你不知道它的存在,蒙蔽你
dealloc:方法中做了一些销毁,收尾工作,比如动态创建的类,需要销毁
_isKVOA: 应该是返回YES
下面附上一张面试题: