iOS KVO

328 阅读3分钟

KVO的能力

KVO全称 key value observing,用于监听对象属性的改变,可以监听多个属性。

使用方法

只需要复写-addObserver:forKeyPath:options:context方法即可,如果监听多个属性,需要在方法中通过keyPath来判断修改的是哪一个属性。在更复杂的业务场景下,使用 context 上下文以及其它辅助手段才能够帮助我们更加精准地确定被观测的对象。尤其是处理那些继承自同一个父类的子类,并且这些子类有相同的 keypath。

// Foo.h
@interface Foo : NSObject

@property (nonatomic, copy) NSString *bar;

@end
// Foo.m
#import "Foo.h"
#import <objc/runtime.h>

@implementation Foo

- (void)setBar:(NSString *)bar
{
    NSLog(@"self->isa = %@", object_getClass(self));
    _bar = bar;
}

ViewController.m
#import "ViewController.h"
#import "Foo.h"
#import <objc/runtime.h>

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Foo *foo = [[Foo alloc] init];
    NSLog(@"self->isa = %@", object_getClass(foo));
    [foo addObserver:self
          forKeyPath:@"bar"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:nil];
    NSLog(@"self->isa = %@", object_getClass(foo));
    NSLog(@"self->isa->superClass = %@", class_getSuperclass(object_getClass(foo)));
    foo.bar = @"祈求者Kael";
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"bar"]) {
        NSLog(@"change = %@", change[NSKeyValueChangeOldKey]);
        NSLog(@"change = %@", change[NSKeyValueChangeNewKey]);
    }
}

@end

2019-05-24 15:49:19.916170+0800 NSCodingDemo[1614:88021] self->isa = Foo
2019-05-24 15:49:19.916548+0800 NSCodingDemo[1614:88021] self->isa = NSKVONotifying_Foo
2019-05-24 15:49:19.916548+0800 NSCodingDemo[1614:88021] self->isa->superClass = Foo
2019-05-24 15:49:19.916667+0800 NSCodingDemo[1614:88021] self->isa = NSKVONotifying_Foo
2019-05-24 15:49:19.916799+0800 NSCodingDemo[1614:88021] change = <null>
2019-05-24 15:49:19.916898+0800 NSCodingDemo[1614:88021] change = 祈求者Kael

实现原理

众所周知,KVO是通过iOS runtime的isa-swizzle来实现的。

从上面代码的打印结果可以看出,一旦使用 addObserver:forKeyPath:options:context:给实例foo添加观察者之后,系统会创建一个新类NSKVONotifying_Foo,并且NSKVONotifying_Foo是继承自Foo。然后把实例foo的isa指针从 Foo 变成了 NSKVONotifying_Foo,这样这个原本的Foo类实际就变成了NSKVONotifying_Foo( 因为isa 指针告诉 Runtime 系统这个对象的类是什么),最后,苹果爸爸还重写NSKVONotifying_Foo的class方法,使它返回父类Foo,造成这个类还是Foo的错觉,从而使上述一系列的骚操作不被察觉。

系统重写了NSKVONotifying_Foo类的setter方法,在调用父类的setter方法前后,插入了willChangeValueForKey:didChangeValueForKey:方法。

正式由于上述,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。

手动触发KVO

手动调用willChangeValueForKey:didChangeValueForKey:方法,即可在不改变属性值的情况下手动触发KVO,willChangeValueForKey:用于记录旧值,didChangeValueForKey用于记录新值。
并且这两个方法缺一不可。
例如,对于属性bar,只有当新值与旧值不同时,才触发观察的代码逻辑。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"bar"]) {
        return NO;
    }
    return YES;
}

- (void)setBar:(NSString *)bar
{
    if ([_bar isEqualToString:bar]) {
        return;
    }
    [self willChangeValueForKey:@"bar"];
    _bar = bar;
    [self didChangeValueForKey:@"bar"];
}

参考