KVO 的基本原理

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 标记此类的作用。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容