KVO 原理:
- 利用Runtime, 动态生成要监听对象的类的子类, 并将要监听的对象的isa指针指向这个新生成的子类
- 重写要监听的keypath对应的set方法, 方法的大概实现类似:
- (void)setAge:(int)age {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"]; // 这句话中,会调用observer的observeValueForKeyPath:ofObject:change:context:
}
- 除此以外, 还会重写 class 方法, 避免新生成的子类的暴露
- 该对象的监听全部被移除后, 将对象的isa指针恢复成原本的类对象 (ps: 如果一个keypath,没有被添加过监听, 那么针对该keypath移除监听, 会导致crash)
直接修改成员变量, 是否能触发kvo的监听
不能, 本质是重写set方法, 直接修改成员变量, 不会调用set方法
如何手动触发kvo的监听
手动调用willChangeValueForKey:方法和didChangeValueForKey:方法, 必须成对调用, 虽然在didChangeValueForKey:方法中会通知到observer,但是如果不事先调用willChangeValueForKey:, didChangeValueForKey:不会起作用
KVO, 自动生成派生子类这套机制,什么情况下不生效
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return false;
}
KVO, 被监听的值的计算由其他值组成, 希望当其他值变化时, 被监听的值也能被通知发生变化, 应该怎么做
例如名字由姓和名组成, 姓或名改变时, name也会改变
// 当给height赋值时, 也会通知到监听age的监听者
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
if([key isEqualToString:@"age"]) {
return [NSSet setWithArray:@[@"height"]];
}
return nil;
}
DEMO:
//
// ViewController.m
// TestKVO
//
// Created by 王昱 on 2021/9/28.
//
#import "ViewController.h"
#import <objc/runtime.h>
#import "WYClassUnit.h"
@interface WYPerson: NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@end
@implementation WYPerson
- (void)setAge:(int)age {
NSLog(@"setAge - begin");
_age = age;
NSLog(@"setAge - end");
}
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@" willChangeValueForKey - begin ");
[super willChangeValueForKey:key];
NSLog(@" willChangeValueForKey - end ");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@" didChangeValueForKey - begin ");
[super didChangeValueForKey:key];
NSLog(@" didChangeValueForKey - end ");
}
// 返回false时, 不会自动生成派生子类
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return true;
}
// 当给height赋值时, 也会通知到监听age的监听者
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
if([key isEqualToString:@"age"]) {
return [NSSet setWithArray:@[@"height"]];
}
return nil;
}
@end
@interface ViewController ()
@property (nonatomic, strong) WYPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person = [WYPerson new];
self.person.age = 1;
NSLog(@"%@", NSClassFromString(@"NSKVONotifying_WYPerson")); // (null)
NSLog(@"%@", object_getClass(self.person)); // WYPerson
NSLog(@"%@", [self.person class]); // WYPerson
// 当执行这句话时, 系统会利用runtime生成一个WYPerson的子类, 名为NSKVONotifying_WYPerson, 并将person的isa指针指向这个WYPerson
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"%@", NSClassFromString(@"NSKVONotifying_WYPerson")); // NSKVONotifying_WYPerson
NSLog(@"%@", object_getClass(self.person)); // NSKVONotifying_WYPerson
NSLog(@"%@", [self.person class]); // WYPerson
// 查看NSKVONotifying_WYPerson的方法列表
// setAge: / class / dealloc / _isKVOA
[WYClassUnit logMethodListWithClass:object_getClass(self.person)];
// 查看setAge:的实现: // (IMP) $0 = 0x00007fff207bf79f (Foundation`_NSSetIntValueAndNotify`)
IMP imp = [self.person methodForSelector:@selector(setAge:)];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@" keypath: %@ \n object: %@ \n change: %@ \n context: %@", keyPath, object, change, context);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
/*
2021-09-28 15:25:27.618362+0800 TestKVO[36052:6763720] willChangeValueForKey - begin
2021-09-28 15:25:27.618490+0800 TestKVO[36052:6763720] willChangeValueForKey - end
2021-09-28 15:25:27.618582+0800 TestKVO[36052:6763720] setAge - begin
2021-09-28 15:25:27.618650+0800 TestKVO[36052:6763720] setAge - end
2021-09-28 15:25:27.618722+0800 TestKVO[36052:6763720] didChangeValueForKey - begin
2021-09-28 15:25:27.618905+0800 TestKVO[36052:6763720] keypath: age
object: <WYPerson: 0x600001b502b0>
change: {
kind = 1;
new = 10;
}
context: (null)
2021-09-28 15:25:27.618985+0800 TestKVO[36052:6763720] didChangeValueForKey - end
*/
self.person.age = 10;
/*
2021-09-28 15:35:25.692740+0800 TestKVO[36628:6776652] willChangeValueForKey - begin
2021-09-28 15:35:25.692810+0800 TestKVO[36628:6776652] willChangeValueForKey - end
2021-09-28 15:35:25.692880+0800 TestKVO[36628:6776652] didChangeValueForKey - begin
2021-09-28 15:35:25.692990+0800 TestKVO[36628:6776652] keypath: age
object: <WYPerson: 0x600002ea80d0>
change: {
kind = 1;
new = 20;
old = 10;
}
context: (null)
2021-09-28 15:35:25.693067+0800 TestKVO[36628:6776652] didChangeValueForKey - end*/
[self.person willChangeValueForKey:@"age"];
self.person->_age = 20;
[self.person didChangeValueForKey:@"age"];
/*
2021-09-28 15:36:26.442258+0800 TestKVO[36687:6778437] didChangeValueForKey - begin
2021-09-28 15:36:26.442313+0800 TestKVO[36687:6778437] didChangeValueForKey - end
*/
self.person->_age = 30;
[self.person didChangeValueForKey:@"age"];
/*
2021-09-28 15:45:49.561074+0800 TestKVO[37298:6795480] willChangeValueForKey - begin
2021-09-28 15:45:49.561166+0800 TestKVO[37298:6795480] willChangeValueForKey - end
2021-09-28 15:45:49.561229+0800 TestKVO[37298:6795480] didChangeValueForKey - begin
2021-09-28 15:45:49.561350+0800 TestKVO[37298:6795480] keypath: age
object: <WYPerson: 0x600002ec8240>
change: {
kind = 1;
new = 1;
old = 1;
}
context: (null)
2021-09-28 15:45:49.561412+0800 TestKVO[37298:6795480] didChangeValueForKey - end
*/
self.person.height = 20;
}
@end