原理篇-KVO

444 阅读3分钟

介绍

KVO是一种机制,当对象(被观察)的某个属性发生更改时,对象可以获得通知,并作出相应处理。那么他是怎么监听的呢?

原理

KVO是用了isa-swizzling来实现的。当对象被kvo观察的时候,此对象的isa指针会改变,指向一个中间的类,而不是它真正的类。然后重写setter方法。

原理的证明

被kvo监听的对象,isa指针指向的中间类是怎样的?

    //1、创建一个Person类,属性变量有age
    self.p2 = [[Person alloc]init];
    self.p2.age = 1;
    
    //2、输出结果:====p2未监听前,isa指针Person
    NSLog(@"====p2未监听前,isa指针%@", object_getClass(self.p2));
    
    //3、p2的age属性添加监听
    [self.p2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld context:nil];
    
    //4、输出结果:====p2监听后,isa指针NSKVONotifying_Person
    NSLog(@"====p2监听后,isa指针%@", object_getClass(self.p2));
    
    //5、移除监听
    [self.p2 removeObserver:self forKeyPath:@"age" context:nil];
    
    //6、输出结果:====p2移除监听后,isa指针Person
    NSLog(@"====p2移除监听后,isa指针%@", object_getClass(self.p2));

由上段代码可以看出,p2被监听前,isa指针指向Person类。在添加监听后,isa指针指向了NSKVONotifying_Person类。

因此,不能用isa指针来获取他真正的类,而是通过class方法来获取。

中间类和之前的类是什么关系呢?

将上面的第4步的输出。多输出一个isa指针指向的类的父类。

    //====p2监听后,isa指针NSKVONotifying_Person====isa指针指向的类的父类指针是Person
    NSLog(@"====p2监听后,isa指针%@====isa指针指向的类的父类指针是%@", object_getClass(self.p2),class_getSuperclass((Class)object_getClass(self.p2)));

由上可看出,生成的中间类NSKVONotifying_Person类是原类Person类的子类。

怎么重写的setter方法来通知呢?

我们手动实现一个KVO来证明~

将kvo自动发送通知改成NO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"age"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

此时,再跑一下程序,会发现当我们改变age的时候,不能再收到通知。也就是以下方法,不会被调用。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@的%@改变了%@", object, keyPath, change);
}
手动发送通知

在Person类中重写setter方法

- (void)setAge: (NSInteger)age {
    
    //在改变值前调用
    [self willChangeValueForKey:@"age"];
    _age = age;
    //在改变值之后调用
    [self didChangeValueForKey:@"age"];
}

OK,在重写setter方法之后,我们又能收到通知了~如果我们想只有在年龄值改变的时候,才收到通知。那我们可以将setter方法改成如下:

- (void)setAge: (NSInteger)age {
    
    if (_age != age) {
        //在改变值前调用
        [self willChangeValueForKey:@"age"];
        _age = age;
        //在改变值之后调用
        [self didChangeValueForKey:@"age"];
    }
}

这样,在我们给age赋值和上次一样的时候,我们不会再收到通知。也就是说下面这种情况只会收到两次通知。

    self.p2.age = 100;
    self.p2.age = 110;
    self.p2.age = 110;

我们再给person加个属性变量birthYear,当age变的时候,birthYear跟着变化。那么setter方法可以写成下面这样

- (void)setAge: (NSInteger)age {
    
    if (_age != age) {
        //在改变值前调用
        [self willChangeValueForKey:@"age"];
        [self willChangeValueForKey:@"birthYear"];
        NSInteger gap = age - _age;
        _age = age;
        _birthYear = _birthYear + gap;
        //在改变值之后调用
        [self didChangeValueForKey:@"age"];
        [self didChangeValueForKey:@"birthYear"];
    }
    
}

这样,当age变的时候,birthYear也会跟着相应变化。