KVO的本质

336 阅读2分钟

kVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象的属性值的改变

基础使用和概念不赘述,我们直入正题。

1、为什么可以监听,他是怎么监听的?

以下列类为例,简写代码如下

self.person = [[HXHPerson alloc] init];
self.person.age = 10;
self.person2 = [[HXHPerson2 alloc] init];
self.person2.age = 100;
//添加kvo监听
[self.person addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];

触发值的变化

self.person.age = 20;
self.person2.age = 300;

接受值的变化

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"object: %@\nchange : %@", object, change);
    }
}
- (void)setAge:(int)age {
    NSLog(@"谁修改了age: %p", self);
}

以上例子,同一个类,不同实例,监听同一个属性 LLDB打印isa

可以看到,添加过observer之后的实例对象的isa指向已经不是类对象 HXHPerson, 变成了 NSKVONotifying_HXHPerson

这就是KVO的机制

NSKVONotifying_HXHPerson是 runtime动态创建的,HXHPerson 的子类.

内部构造的不同:

重写了相应属性的set方法,以上边的age为例,它是去调用了Foundation框架的 _NSSetIntValueAndNotify;

Foundation的源码看不到,但是断点可以看到

可以手动创建一个 NSKVONotifying_HXHPerson 类,这个时候会发现KVO失效了,这也是从侧面证明了上边的结论。

接下来做另一个验证

NSLog(@"person添加监听前 : %p %p",
        [self.person methodForSelector:@selector(setAge:)],
        [self.person methodForSelector:@selector(setAge:)]);
//添加kvo监听
[self.person addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
NSLog(@"person添加监听后 : %p %p",
        [self.person methodForSelector:@selector(setAge:)],
        [self.person methodForSelector:@selector(setAge:)]);

打印输出的方法名前后的变化

2、_NSSetXxxxValueAndNotify的内部实现

伪代码如下

void _NSSetIntValueAndNotify()
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

验证的话可以在willChange和didChage中分别log,不是难事,不再赘述

3、NSKVONotifying_xxx子类的实现

除了重写了setAge之外还重写了class,dealloc,_isKVO三个方法。 代码验证

- (void)printMethodNamesOfClass:(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(@"\n%@ %@", cls, methodNames);
}

在使用的地方调用

[self printMethodNamesOfClass:object_getClass(self.person)];
[self printMethodNamesOfClass:object_getClass(self.person2)];

log截图如下

3.1、class方法

class方法直接返回原来的类对象,object_getClass()返回真实的当前类对象

为什么要重写这个方法?
不希望暴露这个类,不暴露内部实现。那有没有别的原因呢,目前没想到。

4、总结

本质就是runtime动态生成NSKVONotifying_xxx子类,并让instance对象的isa指向这个全信的子类,当修改instance对象的属性时会调用_Foundation的_NSSetXXXValueForKey函数,此函数内部会依次调用:
willChangeValueForKey;
父类原来的setter;
didChangeValueForKey;
内部触发Observer的监听监听方法

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;