Foundation01 - KVO

109 阅读4分钟

KVO 原理:

  1. 利用Runtime, 动态生成要监听对象的类的子类, 并将要监听的对象的isa指针指向这个新生成的子类
  2. 重写要监听的keypath对应的set方法, 方法的大概实现类似:
- (void)setAge:(int)age {
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"]; // 这句话中,会调用observer的observeValueForKeyPath:ofObject:change:context:
}
  1. 除此以外, 还会重写 class 方法, 避免新生成的子类的暴露
  2. 该对象的监听全部被移除后, 将对象的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