KVO全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
基本用法回顾:
Person.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,assign) int age;
@end
Person.m文件
#import "Person.h"
@implementation Person
@end
ViewController.h文件
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
ViewController.m文件
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic,strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person = [[Person alloc] init];
self.person.age = 10;
//给person对象添加kvo监听
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];//context可以传nil
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.age = 20;
}
//当监听对象的属性值发生改变,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@,context = %@",object,keyPath,change,context);
打印结果:
2023-03-16 21:48:49.819982+0800 test[95000:123860664] 监听到<Person: 0x6000029449e0>的age属性值改变了 - {
kind = 1;
new = 20;
old = 10;
},context = 123
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
探究KVO的本质
Person.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,assign) int age;
@end
Person.m文件
#import "Person.h"
@implementation Person
- (void)setAge:(int)age{
_age = age;
}
@end
把ViewController.m的文件修改一下,如下:
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic,strong) Person *person1;
@property (nonatomic,strong) Person *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
//给person对象添加kvo监听
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];//context可以传nil
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person1.age = 21;
// self.person2.age = 22;
[self.person1 setAge:21];
[self.person2 setAge:22];
}
//当监听对象的属性值发生改变,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@,context = %@",object,keyPath,change,context);
}
打印结果:
2023-03-16 21:55:59.517205+0800 test[95201:123868636] 监听到<Person: 0x600002c946f0>的age属性值改变了 - {
kind = 1;
new = 21;
old = 1;
},context = 123
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"age"];
}
@end
思考为什么 [self.person1 setAge:21];
代码执行完就走监听对象的回调方法, [self.person2 setAge:22];
代码执行完就不会走监听对象的回调方法呢?因为setAge方法都是一样的,那么问题是不是就出现在两个对象的本身呢?下面就找一下这两个对象有什么不一样
在[self.person1 setAge:21];
代码处打一断点运行代码,在lldb输入p self.person1.isa
和p self.person2.isa
会发现打印出来的结果为
(lldb)p self.person1.isa
(Class) $0 = NSKVONotifying_Person
Fix-it applied, fixed expression was:
self.person1->isa
(lldb)p self.person2.isa
(Class) $1 = Person
Fix-it applied, fixed expression was:
self.person2->isa
上面的打印结果说明person1的类对象是NSKVONotifying_Person,person2的类对象是Person
NSKVONotifying_Person是runtime运行时动态创建的一个类,是Person的一个子类
未使用KVO监听的对象:
使用了KVO监听的对象:
大概写一下动态创建的NSKVONotifying_Person.m文件的伪代码帮助理解一下是怎么调用的过程
下面验证一下内部逻辑:
NSLog(@"person1添加KVO监听之前 - %@ %@",object_getClass(self.person1),object_getClass(self.person2));
//给person对象添加kvo监听
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];//context可以传nil
NSLog(@"person1添加KVO监听之前后 - %@ %@",object_getClass(self.person1),object_getClass(self.person2));
打印结果:
2023-03-16 22:42:32.916270+0800 test[96843:123920985] person1添加KVO监听之前 - Person Person
2023-03-16 22:42:32.917812+0800 test[96843:123920985] person1添加KVO监听之前后 - NSKVONotifying_Person Person
NSLog(@"person1添加KVO监听之前 - %p %p",[self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
//给person对象添加kvo监听
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];//context可以传nil
NSLog(@"person1添加KVO监听之后 - %p %p",[self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
打印结果:
2023-03-16 22:46:20.932383+0800 test[96992:123926093] person1添加KVO监听之前 - 0x104d132a0 0x104d132a0
2023-03-16 22:46:20.933645+0800 test[96992:123926093] person1添加KVO监听之后 - 0x10e3b4cfb 0x104d132a0
添加监听之前,打印的调用的setAge的地址是一样的,监听之后,调用的setAge的地址是不一样的,person2的setAge的地址前后都是一样的,
打断点之后在lldb输入p (IMP) 0x104d132a0
,这里的0x104d132a0地址是上边NSLog出来的地址直接粘贴过来的,然后lldb会显示出
p (IMP) 0x10e3b4cfb
,0x10e3b4cfb地址是person1添加监听之后的setAge方法的地址,下边验证一下NSKVONotifying_Person和Person的元类对象,即他们两个的isa都指向了哪里。
//给person对象添加kvo监听
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];//context可以传nil
NSLog(@"类对象 - %p %p",object_getClass(self.person1),object_getClass(self.person2));//self.person1.isa,self.person2.isa
NSLog(@"元类对象 - %p %p",object_getClass(object_getClass(self.person1)),object_getClass(object_getClass(self.person2)));//self.person1.isa.isa,self.person2.isa.isa
打印结果:
2023-03-16 23:08:38.816133+0800 test[97492:123946024] 类对象 - 0x60000299cd80 0x1020f1748
2023-03-16 23:08:38.816421+0800 test[97492:123946024] 元类对象 - 0x60000299d290 0x1020f1720
地址都不一样,说明NSKVONotifying_Person有自己的元类对象,Person也有自己的元类对象,他俩所指向的元类对象不是同一个
下边验证一下NSKVONotifying_Person类对象和Person类对象里都有哪些方法,是不是如截图所示:
[self printMethodNameOfClass:object_getClass(self.person1)];
[self printMethodNameOfClass:object_getClass(self.person2)];
- (void)printMethodNameOfClass:(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);
}
打印结果:
2023-03-16 23:45:06.174539+0800 test[98569:123980988] NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,
2023-03-16 23:45:06.174671+0800 test[98569:123980988] Person setAge:, age,
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSet***ValueAndNotify函数:willChangeValueForKey:、父类原来的setter、didChangeValueForKey:,在didChangeValueForKey:内部会触发监听器(Observer)的监听方法:observeValueForKeyPath: ofObject:change:context:
如何手动触发KVO?
正常情况下都是有人去修改对象的属性才会自动触发KVO,有些情况下,可能没有修改对象的属性值,但是想走监听里边的逻辑,那么就手动去触发,写如下两行代码就可以出发KVO的监听了
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
直接修改成员变量会触发KVO吗?
不会触发KVO
KVO的效率高还是代理的效率高?
代理的效率高,因为代理直接调用,中间不存在动态生成类
能不能KVO监听的前提是这个属性有没有set方法,因为是在set方法里做操作,重写set方法,让他得以监听到属性的修改。