理解KVO

相信KVO对大家来说都不陌生,即使你没有使用过,基本也都曾了解过,听说过。
这篇文章作者就带大家来深入研究一下KVO的实现原理,以及使用,目的是让大家真正的理解KVO。

一. 了解方法

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;  
注册监听者
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context; 
实现监听方法
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context; 
注销监听

看到官方给我们听过的几个方法有没有觉得和通知、代理很类似?
其实就是一样的用法特别简单,共分为三个步骤:

  • 注册
  • 监听
  • 移除

二. 注册监听者

我们来看注册监听方法

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;  
我们来看几个参数:
  • observer: 监听对象(就是谁来监听KVO事件)
  • keyPath: 监听属性的名字
  • context: 添加监听方法的最后一个参数,是一个可选的参数,可以传任何数据,这个参数最后会被传到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。如果你要用context来区分不同的监听知,一个推荐的做法是声明一个静态变量,其保持它自己的地址,这个变量没有什么意义,但是却能起到区分的作用,如下:
static void *ObservePersonContext = &ObservePersonContext;
  • NSKeyValueObservingOptions: 监听选项是个枚举,我们来看一下都有那些类型可选
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {

    NSKeyValueObservingOptionNew = 0x01,
    NSKeyValueObservingOptionOld = 0x02,
    NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
    NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08

};
  • NSKeyValueObservingOptionNew:提供更改前的值
  • NSKeyValueObservingOptionOld:提供更改后的值
  • NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
  • NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(即一次修改有两次触发)
下面我们来看一段代码
#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong) Person *richPerson;

@end

@implementation ViewController

static void *ObservePersonContext = &ObservePersonContext;

- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    // 1. 首先我们先实例化一个Person对象
    Person *richPerson = [[Person alloc] init];
    richPerson.name = @"光头强";
    richPerson.age = 38;
    self.richPerson = richPerson;
    [self.richPerson addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior context:ObservePersonContext];
    
    self.richPerson.age = 50;
}

以上就是一个简单的监听注册,这里大家一定要切记context的值,当监听多个对象,以及分类也使用了KVO等情况,强烈建议大家使用context来做区分。以免造成后期维护成本过高的影响。

三. 监听

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

我们来了解一下监听方法中的几个参数

  • keyPath: 我们监听变化的属性名字。
  • object: 我们监听的对象。
  • change: 一个存储我们需求的字典类型容器,根据注册时我们选择Option来传递给我们想要的数据,有五个常量作为它的键值:
* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
  一般情况下返回的都是1也就是第一个NSKeyValueChangeSetting,但是如果你监听的属性是一个集合对象的话,当这个集合中的元素被插入,
  删除,替换时,就会分别返回NSKeyValueChangeInsertion,NSKeyValueChangeRemoval和NSKeyValueChangeReplacement。

* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
  被监听属性改变后新值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,
  该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值)。

* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
  被监听属性改变前旧值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,
  该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值)

* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
  如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,
  这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index。

* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey
  如果注册监听者是options中指明了NSKeyValueObservingOptionPrior,change字典中就会带有这个key,值为NSNumber类型的YES.
  • context: 同步骤二注册!

四. 注销监听

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

所谓注销就和通知一样,在我们编写逻辑的View或Controller销毁的时候,注销掉我们的监听。
当你向一个不是监听者的对象发送remove消息的时候(也可能是,你发送remove消息时,接受消息的对象已经被remove了一次,或者在注册为监听者前就调用了remove),xcode会抛出一个NSRangeException异常,所以,如果为了保险起见,可以把remove操作放在try/catch中。

注意:

一个监听者在其被销毁时,并不会自己注销监听,而给一个已经销毁的监听者发送通知,会造成野指针错误。所以至少保证,在监听者被释放前,将其监听注销。保证有一个add方法,就有一个remove方法。

OK,如果你认真看完了以上内容,恭喜你,你已经完全知道该如何来使用KVO了!
但是这部是本篇内容的主旨。但要深刻理解,我们继续往下看!

五. KVO的实现原理

先上两张截图方便大家参考


1.png

2.png

细心的你早已发现NSObject里的isa指针改变了
根据两张图我们可以看出,指向了一个看着眼熟似曾相识的类(NSKVONotifying_Person)。
没错!这个就是系统帮我们创建的Person的子类

愿意一探究竟的小伙伴可以自己在以上例子的基础上,创建一个名字为NSKVONotifying_Person的类,编译不会有任何问题,因为OC是运行时语言,在运行程序到注册监听的时候,系统会通过runtime的方法创建一个名字为NSKVONotifying_Person的类,但是这时程序会导致崩溃。
因为我已经手动创建了这个类。

当然感兴趣的小伙伴也可以使用runtime的方法在注册监听之前,动态创建NSKVONotifying_Person类,也会产生同样的问题!

总结

KVO 很强大,没错。知道它内部实现,或许能帮助更好地使用它,或在它出错时更方便调试。但官方实现的 KVO 提供的 API 实在不怎么样。

比如,你只能通过重写 -observeValueForKeyPath:ofObject:change:context: 方法来获得通知。想要提供自定义的 selector ,不行;想要传一个 block ,门都没有。而且你还要处理父类的情况 - 父类同样监听同一个对象的同一个属性。但有时候,你不知道父类是不是对这个消息有兴趣。虽然 context 这个参数就是干这个的,也可以解决这个问题 - 在 -addObserver:forKeyPath:options:context: 传进去一个父类不知道的 context。但总觉得框在这个 API 的设计下,代码写的很别扭。至少至少,也应该支持 block 吧。

所以在实际开发中 KVO 使用的情景并不多,更多时候还是用 Delegate 或 NotificationCenter。
后续作者会在进阶篇讲解自定义KVO的实现,敬请关注!

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

推荐阅读更多精彩内容