Let hearsay KVO no longer far away

KVO , a noun that is not unfamiliar, is so simple to use.

But do you really understand it? Why does this thing exist? Do you understand the logic behind it? What kind of attitude should we use to be considered elegant? That's what this article will give you the insight you want.

official statement

The implementation of KVO has not been officially disclosed. What we can see is this description:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

It translates like this:

Key-Value Observing 实施细节

Automatic key-value observing 是使用称为 isa-swizzling 的技术实现的。

顾名思义,isa指针指向维护分派表的对象类。这个分派表本质上包含指向类实现的方法的指针,以及其他数据。

当一个观察者注册一个对象的属性时,被观察对象的isa指针会被修改,指向一个中间类,而不是真正的类。因此,isa指针的值不一定反映实例的实际类。

永远不要依赖isa指针来确定类成员身份。相反,应该使用class方法来确定对象实例的类。
复制代码

In summary, it roughly expresses the meaning:

  • KVOkey-value observingfull name
  • KVOIt is implemented with isa-swizzlinga , do we know how this technology is implemented, but we know what it can achieve, and the further explanation below is his role;
  • This technique directly changes the isapointer , and then points to an intermediate class instead of the class corresponding to the object itself;
  • Since the isapointer can be changed, it isais not allowed to use it as his ID card. After closing the door, open a window and use the classmethod to determine his identity.

Let's start with the official statement to verify it.

Basic operation

When we use KVO, we can usually do this:

Person * onePerson = [[Person alloc] init];
// 尝试更改 age 的值
onePerson.age = 1;

// self 监听 onePerson 的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[onePerson addObserver:self forKeyPath:@"age" options:options context:nil];
// 再次尝试更改 age 的值
onePerson.age = 10;
[onePerson removeObserver:self forKeyPath:@"age"];
复制代码

We added the above code in the ViewControllerappropriate place, obviously, we have added a listener to the property ofPerson the object of the class , yes ( ).onePersonageobserverselfViewController

Then, we can then implement the listener callback method:

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

    NSLog(@"Got!!对象 %@ 属性 %@ 发生了改变 :%@", object, keyPath, change);

}
复制代码

ok, the preparation is ready, let's start the performance.

Who is isa pointing to?

By convention, throwing hands is a breakpoint. Where is the break? Because we need to understand the changes KVObefore , the power outage is added addObserverat , which is here:

image.png

run 起来,我们在断点处及单步运行后,分别打印一下 onePersonisaimage.png

很明显,isa 的指向确实发生了变化,在 addObserver 之后,onePersonisa 指向了一个叫 NSKVONotifying_Person 的类,这个类并不是我们自己定义的,而是在我们执行 addObserver 之后才生成的,也就是通过 runtime 创建的一个类,且在其类名中还能找到 Person 的影子,确切的说就是加了一个前缀。

目前看来,确实如官方说法一样,生成了一个中间类,可是却并没有交代这个新生成的类是何许人,与原类的关系。看类名又似乎是有关联的。

倒是有个道听途说的说法:新生成的类是原类的子类

我们不妨加以验证一下,同样的手法看下其 superclass

image.png

这里看到的都是 NSObject。那是不是说明上面提到的子类说法不成立呢?

其实不然,我们知道 superclass 拿到的可能并不是准确的,比如要是重写了呢?我们有个更准确的获取方式,同样的手法:

image.png

可以看到,object_getClass() 获取到的跟 isa 是一样的,如果查看 runtime 源码就会知道,其实际上就是返回的 isa。而从 class_getSuperclass() 的结果我们可以看到 NSKVONotifying_Person 的父类确实是 Person

总结来说,上面道听途说的说法还是靠谱的,新生成的类确实是原类的子类,在命名上其实就是在原类的类名上加上了前缀 NSKVONotifying_

class

上面我们通过 superclass 拿到的前后结果一致,从另一个方面正好是说明了 NSKVONotifying_Person 类中其实是重写了 superclass 方法实现的。

而我们从官方说法中提到的 class 了解到,我们通过 class 获取对象的类别实际上是更准确的。这里怎么理解呢?

不妨同样的手法,再来一次:

image.png

可以看到,classsuperclass 的输出如出一辙。说明什么呢?说明 class 方法也是被重写了。

那么问题来了,这样看其实 class 拿到的并不是准确的结果啊,为啥官方推荐呢?

有句话说的是,你看到的只是它想让你看到的,这也就是官方希望的。

换句话说,官方其实并不想暴露 NSKVONotifying_Person 类的存在,只是为了实现 KVO 的一种手段,让外界看起来世界和平,而他自己干了他爱干的事儿。

所以,了然否?

setter

我们还曾听说,KVO 的实现,最终是通过重写 setter 方法实现的。是不是呢?

来来来,手法不变,我们再来一次:

image.png

我们依旧是在 addObserver 前后打印的相关信息,是 setter 方法的 IMP 地址及其相关信息。

不难看出在这前后,setter 方法的实现确实发生了变化,之前我们根据其信息能知道,其调用的是正常的 setAge: 方法,而之后呢?Foundation _NSSetUnsignedLongLongValueAndNotify 是什么玩意儿呢?

至少能知道就是因为这个实现了 KVO 在值改变前后的通知操作,而其中 UnsignedLongLong 应该跟我们属性的数据类型有关,按说应该还有一堆类似的方法,我们来确认一下。

我们把 age 的数据类型改一下看看,比如改成 double 之后:

image.png

其关键字或者叫函数名也发成了变化,对应上了 double,可以猜测 Foundation 内部确实有一堆对应各种数据类型的函数。有说通过 nm 命令能查出来的,反正我这在最新的 SDK 中没有查到,但是并没有啥子影响。

肯定的是,这些个函数都做了同一个事儿,什么事儿呢?就是在 setAge 的同时,也在设置值的前后进行了通知。

我们通常这样,在Person 类中加上下面的代码:

- (void)willChangeValueForKey:(NSString *)key {
    NSLog(@"Got!!Will change value for key :%@", key);
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"Got!!Did change value for key :%@", key);
    [super didChangeValueForKey:key];
}
复制代码

我们去掉断点,再跑一下:

image.png

我们很明显的可以看到,在添加监听之后触发 setAge: 的时候,先后调用了 PersonwillChangeValueForKey:didChangeValueForKey: 方法以及 ViewContrillerobserveValueForKeyPath:ofObject:change:context: 方法。

所以,KVO 确实是重写了 setter 方法,其目的就是为了在改变属性值的前后通过 willChangeValueForKey:didChangeValueForKey: 实现通知,及触发 observeValueForKeyPath:ofObject:change:context: 的回调。

手动触发 KVO

既然上面提到了 willChangeValueForKey:didChangeValueForKey:,那我们自己调用会有什么后果呢?因为官方并没有限制我们对其调用,比如把代码改成下面这样:

// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[onePerson addObserver:self forKeyPath:@"age" options:options context:nil];

// 暂时注释通过 setter 更改 age 的值
// onePerson.age = 10;

// 手动调用
[onePerson willChangeValueForKey:@"age"];
[onePerson didChangeValueForKey:@"age"];
复制代码

这里我特意注释掉了通过 setter 方法对 age 属性赋值,我们跑一下看看。 image.png

首先来看,这里手动调用并没有什么问题,跟之前的日志也一模一样(其实还是有一点点差别,后面讲)。

但是,问题也就在一模一样,问题是:observeValueForKeyPath:ofObject:change:context: 怎么触发的?

进一步注释掉 willChangeValueForKey: 就能确认,其就是 didChangeValueForKey: 方法触发的,也就是说 KVO 重写 setter 之后,并不是在重写的 setter 方法内部触发 observeValueForKeyPath:ofObject:change:context: 的回调,触发实际上发生在 didChangeValueForKey: 内部。

所以虽然我们可以手动调用上述的方法,但是还是需要谨慎,因为其会触发 observeValueForKeyPath:ofObject:change:context: 的回调,实际上就算是触发可能也没关系,关键是你瞅瞅回调之后的日志,其值前后其实并没有变化啊(就是这里不一样),这可能就是问题了,这其实就是一次错误的触发了。

KVO 可能的实现

到这里,我们其实可以大致了解 KVO 的内部实现了,类似下面这样(只是伪代码):

- (void)setAge:(NSUInteger)age {
    // 这里就该是根据类型调用不同的函数了,这里只看了 age
    _NSSetUnsignedLongLongValueAndNotify()
}

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key {
    [super didChangeValueForKey:key];
    /// 触发回调
    [observer observeValueForKeyPath:key ofObject:xxx change:xxx context:xxx];
}

void _NSSetObjectValueAndNotify() {
    [self willChangeValueForKey:@"age"];
    // 调用 Person 的 setter 方法
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}
复制代码

优雅的使用 KVO

上面我们说到,在添加了 KVO 的属性中,在修改属性值后会毁掉方法 observeValueForKeyPath:ofObject:change:context:,那我们在添加多个这样的属性之后呢?我们就需要在上面回调方法中增加一堆 if-else 的判断了,这样显然并不优雅。

这里实际上是有一优雅的姿势来使用 KVO 的,它就是出自 FBKVOController, 在这里

也可以研究下其封装 KVO 的逻辑,也有不少收获,有机会我们后面再来聊一聊。

最后

ok,以上,希望我们都能有所收获。

Guess you like

Origin juejin.im/post/7086666658793652232