iOS 学习历程

KVO 的基本原理

2018-03-25  本文已影响8人  人话博客

KVO 的基本原理

KVO 是 key/value/observer 的缩写。
表示的意思是:当某个属性的值发生变化的时候,通知观察者。
在直白一点,当某个对象的属性调用 setter 方法的时候,通知观察者。

这里面有个 观察者,所以,KVO 的本质上就是一个观察者模式。


观察者模式

定义:
事件的发布者发布事件并执行事件,订阅者订阅事件并提供事件响应函数。

观察者设计模式

一个事件机制包含:

  1. 事件发布者。
  2. 事件订阅者。
  3. 事件发布者发布并执行事件。
  4. 事件订阅者订阅并提供事件响应函数。

对应到 iOS 中的 KVO。

[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];


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

self.person 事件发布者 -> self.person 当前 Person 对象
addObserver:self 事件订阅者 -> self 当前控制器
self.person setName: --> 事件发布者发布并执行事件。
**- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> )change context:(void )context --> 事件订阅者订阅并提供事件响应函数。

针对于 iOS 的 KVO 来说,可以这么简单的理解。

当期控制器作为观察者订阅 Person 的某个属性的 setXXXX: 方法。
当这个 setXXXX: 方法执行过程中,会调执行事件,执行控制器提供的事件响应方法。


KVO 的基本使用

当前控制器订阅某个对象的某个属性的改变

注册事件订阅者,观察对象的属性。

[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];

事件订阅者提供事件响应函数。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@",change);
}

注意:KVO 使用注意移除事件观察者。

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
}


KVO的调试

代码执行到 28 行。


image.png

当前 self.person 的类型

image.png

执行到 29 行之后。


image.png

当前 self.person 的类型。

image.png

如果看不到这个类型,可以 cmd + q 退出 xcode 在重新打开一次。

发现当前 self.person 的 isa 指针指向了 NSKVONotifying_RLPerson 类型。


NSKVONotifying_RLPerson

在执行了 [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil]; 这行代码之后,self.person 的 isa 指针变成了 NSKVONotifying_RLPerson 类型了。

目前为止,不清楚,为什么会有这么一个类型。
但可以通过 runtime 的方式,来获取这个类型的一些属性和方法看看。

查看 NSKVONotifying_RLPerson 类的属性。

 [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
    
    // 当29行代码执行完毕,self.person 的 isa 指针指向了 NSKVONotifying_RLPerson 类型。
    Class newClass = NSClassFromString(@"NSKVONotifying_RLPerson");
    unsigned int count;
    objc_property_t *ps = class_copyPropertyList(newClass, &count);
    NSLog(@"-----NSKVONotifying_RLPerson 属性类表 BEGIN----");
    for (NSInteger i = 0; i < count; i++) {
        NSLog(@"%s",property_getName(ps[i]));
    }
    NSLog(@"-----NSKVONotifying_RLPerson 属性类表 END----");

运行结果:

2018-03-25 01:11:17.008 KVO-CC[33470:16404323] -----NSKVONotifying_RLPerson 属性类表 BEGIN----
2018-03-25 01:11:17.008 KVO-CC[33470:16404323] -----NSKVONotifying_RLPerson 属性类表 END----

发现,此类好像没有属性?

查看 NSKVONotifying_RLPerson 类的方法。

Method *ms = class_copyMethodList(newClass, &count);
NSLog(@"-----NSKVONotifying_RLPerson 方法列表 BEGIN----");
for (NSInteger i = 0; i < count; i++) {
        NSLog(@"%@", NSStringFromSelector(method_getName(ms[i])));
}
NSLog(@"-----NSKVONotifying_RLPerson 方法列表 END----");

运行结果:

2018-03-25 01:19:04.595 KVO-CC[33532:16417767] -----NSKVONotifying_RLPerson 方法列表 BEGIN----
2018-03-25 01:19:04.595 KVO-CC[33532:16417767] setName:
2018-03-25 01:19:04.595 KVO-CC[33532:16417767] class
2018-03-25 01:19:04.596 KVO-CC[33532:16417767] dealloc
2018-03-25 01:19:04.596 KVO-CC[33532:16417767] _isKVOA
2018-03-25 01:19:04.596 KVO-CC[33532:16417767] -----NSKVONotifying_RLPerson 方法列表 END----

发现 NSKVONotifying_RLPerson 中有 4个方法。

  1. setName:
  2. class
  3. dealloc
  4. _isKVO

反正就是由4个方法。其中一个还是 setName:
在我们添加 KVO 的时候,被观察的对象 RLPerson 里有就这么一个方法。
理性感觉上,NSKVONotifying_RLPerson 应该是这个类的子类,否则,为什么这么巧合,它也有一个 setName: 的方法?

查询 KSNVONotifying_RLPerson的父类是否是 RLPerson

在此之前,要先了解一张图。补充一下知识点。


image.png

图片的最左边,就是我们经常用到的 instance 实例对象。
实例对象的虚线(isa)直接指向了是当前实例的类型,也就是类(对象)
在类对象的右边,仍然有一条(isa)指针,它指向的是 metaClass。 也就是基元类。

前半段很好理解。

RLPerson *p = [RLPerson new];

p 是实例,RLPerson 是 P 实例的类型。

但是对后半段的解读则是:在某种程度上,RLPerson 也是对象,它是由它的基元类实例化出来的对象。

类对象和基元类对象的异同?

  1. 类对象一般会存储实例对象的一些 Method。也就是实例方法。
  2. 基元对象,是类对象的模板,它可以实例化出来类对象(不是实例对象)。类对象的类方法,一般是存储在它的基元类当中的。

上面只是补充知识点,以免自己遗忘。


4个方法中的 setName:

因为 NSKVONotifying_RLPerson 里面有个 setName: 而 RLPerson 里面也有个 setName: 这应该不是巧合,它俩之间应该存储某种关系?而且应该是父子类关系。

// 获得NSKVONotifying_RLPerson 的父类
Class superClass = class_getSuperclass(newClass);
NSString *superClassName = NSStringFromClass(superClass);
    
NSLog(@"NSKVONotifying_RLPerson 的 父类是 : %@",superClassName);

运行结果:

2018-03-25 11:06:12.138 KVO-CC[33712:16460834] NSKVONotifying_RLPerson 的 父类是 : RLPerson

确定了,NSKVONotifying_RLPersonRLPerson 的子类,也就确定了RLKVONotifying_RLPersonsetName: 是来自 RLPersonsetName:继承。

那这个 setName : 是单纯的继承过来,还是自己又做了什么额外的处理。

每个赋值属性本质上 setter ,setter 里面可能有负责的逻辑,可能也只是一个简单的赋值。
但在添加了 KVO 之后,原来对象的 setter 逻辑不变,会在此逻辑的上下各加一行代码,完成 KVO 的触发。

RLKVONotifying_RLPerson 重写的 setName:逻辑

[self willChangeValueForKey:@"name"];
[super setName:xxxxxx]; // RLPerson 自己的处理 setter 逻辑
[self didChangeValueForKey:@"name"];

证明 KVO 的执行发布通知的核心,就是这上下两行代码。

首先把 person 对象的 age 观察添加上。

[self.person addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:nil];

但是,不去触发这个属性的 KVO。

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

运行结果:

2018-03-25 12:01:25.799 KVO-CC[34213:16541280] {
    kind=1,
    new=李四**,

}
2018-03-25 12:01:26.021 KVO-CC[34213:16541280] {
    kind=1,
    new=李四***,
}

KVO 只对 name 属性生效。

实现 age 属性的 setAge:方法,并加上上述两行 KVO 的核心代码。

- (void)setAge:(NSUInteger)age {
    [self willChangeValueForKey:@"age"];
    _age = age;
    [self didChangeValueForKey:@"age"];
}

运行结果:

image.png

所以,KVO 的执行确实是由 [self willChangeValueForKey:@"XXX"]; && [self didChangeValueForKey:@"XXX"]; 两行代码生效的。

上面这段,也就是常说的 手动触发 KVO。

总结一下手动调用 KVO 基本步骤:

  1. 仍然需要把属性注册到 KVO 机制里面。
  2. 在当前对象中重写 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 告知,那些属性不需要自动触发 KVO。
  3. 在属性的 setXXX: 方法里,手动调用 [self willChangeValueForKey:@"XXX"]; && [self didChangeValueForKey:@"XXX"];

4个方法中的class

在 RLPerson 对象添加 KVO 观察之后,类型已经变成了 NSKVONotifying_RLPerson 了。
那么,在 NSKVONotifying_RLPerson 里的 class 方法是啥意思呢?

按道理来说,既然 RLPerson 对象的 isa 指指向了 NSNotifying_RLPerson 它返回的 class 类型应该也是 NSKVONotifying_RLPerson 才对。

// RLPerson 在添加 KVO 之后,类型变成了 NSKVONotifying_RLPerson 了。但此新类型中,有一个 class 的方法。
    NSString *className = NSStringFromClass([self.person class]);
    NSLog(@"className : %@",className);

而实际情况是:

className : RLPerson

仍然是原来的 RLPerson 类型。

所以在 NSNotifying_RLPerson 这个类中重写 class 方法,本质就是返回当前对象原本的类型。苹果想隐藏 KVO 的内部实现细节。


4个方法中的 dealloc

因为,毕竟创建了一个新的类型,在取消观察者的时候,会把这个新的类型删除。所以,实际就在这个 dealloc 方法里。


4个方法中的 _isKVO

看文档和博客,说的是,标记这个新类 KVO 机制新建的。


基本总结

KVO 的实现基本原理:

  1. 创建当前类的子类,类型为 NSNotifying_类名。
  2. 在此类中,重写了4个方法 setXxxx: class ,dealloc ,_isKVO
  3. setXXXX: 内部保留了原来的 setXXX 逻辑之外,在上下都加了两行 self willChangeValueForKey:@"XXX"]; && [self didChangeValueForKey:@"XXX"];
  4. 其他3个方法,class 隐藏KVO 内部实现细节、dealloc 释放 KVO 产生的新类资源、_isKVO 标记此类的作用。
上一篇下一篇

猜你喜欢

热点阅读