KVO解析(二) —— 一个简单的KVO实现

版本记录

版本号 时间
V1.0 2017.09.14

前言

KVO具有更强大的功能,是苹果给我们的一个回调机制,在某个对象注册监听者后,在被监听的对象发生改变时,对象会发送一个通知给监听者,以便监听者执行回调操作。接下来几篇就详细的解析一下KVO。感兴趣的可以看上面几篇。
1. KVO解析(一) —— 基本了解

键值观察步骤

为确保对象可以接受键值观察的通知,必须执行如下步骤:

还要注意:并非所有类都符合所有属性的KVO。 您可以按照KVO合规性中描述的步骤确保您自己的类符合KVO。 通常,如果Apple提供的框架中的属性通常被记录,那么它们符合KVO的合规性。


注册观察者

观察对象首先通过发送一个addObserver:forKeyPath:options:context:消息来注册观察对象,将自身传递给观察者和要观察的属性的关键路径。 观察者另外指定一个选项参数option和一个上下文指针context来管理通知的各个方面。

1. Options

指定为选项常量的按位OR的options参数影响通知中提供的change字典的内容以及生成通知的方式。

您选择通过指定选项NSKeyValueObservingOptionOld从更改之前接收观察到的属性的值。 您使用选项NSKeyValueObservingOptionNew请求属性的新值。 您可以使用这些选项的按位OR来收到旧值和新值。

您指示被观察的对象发送立即更改通知(在addObserver:forKeyPath:options:context:returns之前),其选项为NSKeyValueObservingOptionInitial。 您可以使用此额外的一次性通知来建立观察者中的属性的初始值。

您可以通过包含NSKeyValueObservingOptionPrior选项来指示观察对象在属性更改之前发送通知(除更改之后的通常通知之外)。 change更改字典表示通过将NSKeyValueChangeNotificationIsPriorKey的关键字与NSNumber包装YES的值相关联的代替通知。 那个key是不存在的。 当观察者自己的KVO合规性要求它调用一个取决于所观察属性的其中一个属性的-willChange ...方法时,可以使用预置通知。 通常的修改后通知太迟了,无法及时调用willChange ...

2. Context

addObserver:forKeyPath:options:context:方法中的上下文指针包含将在相应的更改通知中传回给观察者的任意数据。 您可以指定NULL并完全依赖于键路径字符串来确定更改通知的来源,但是由于不同的原因,此方法可能会导致超类也遵循相同键路径的对象的问题。

更安全和更可扩展的方法是使用上下文来确保您收到的通知注定给您的观察者,而不是超类。

你的类中唯一命名的静态变量的地址是一个很好的上下文。 在超类或子类中以类似方式选择的上下文将不太可能重叠。 您可以为整个类选择单个上下文,并依靠通知消息中的键路径字符串来确定更改的内容。 或者,您可以为每个观察到的键路径创建一个独特的上下文,这样可以避免完全需要字符串比较,从而实现更有效的通知解析。 下面显示了以这种方式选择的balanceinterestRate属性的示例上下文。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

下面的例子,展示的是一个Person对象是如何利用给定的上下文指针注册自己作为Account对象balanceinterestRate属性的观察者的。

- (void)registerAsObserverForAccount:(Account*)account 
{
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}

这里还要注意:键值观察addObserver:forKeyPath:options:context:方法不保持对观察对象,被观察对象或上下文的强引用。如果需要的话, 您应该确保在必要时保持对观察,被观察,对象和上下文的强烈引用。


接收Change的通知

当被观察对象属性的值发生变化时,观察者会接收到observeValueForKeyPath:ofObject:change:context:消息。 所有观察者必须实现这种方法。

被观察对象提供了键值路径并触发通知,并把自己作为关联对象,还有关于改变的详细内容的字典、当观察者注册此键值路径时提供的上下文指针。

更改字典条目NSKeyValueChangeKindKey提供有关发生的更改类型的信息。 如果观察到的对象的值已更改,NSKeyValueChangeKindKey条目将返回NSKeyValueChangeSetting。 根据注册观察者时指定的选项,更改字典中的NSKeyValueChangeOldKeyNSKeyValueChangeNewKey条目包含更改前后的属性值。 如果属性是对象,则直接提供该值。 如果属性是标量或C结构体,则该值将包装在NSValue对象中(与键值编码KVC一样)。

如果观察到的属性是一对多关系,NSKeyValueChangeKindKey条目还指示关系中的对象是否分别插入,删除或替换,分别返回NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement

下面展示的是Person观察者实现的方法observeValueForKeyPath:ofObject:change:context:,并输出balanceinterestRate属性代表的新旧值。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context 
{
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } 
    else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } 
    else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}

当你注册观察者时指定的上下文是NULL时,您将通知的键路径与您观察到的键路径进行比较,以确定发生了什么变化。 如果您对所有观察到的键路径使用单个上下文,则首先根据通知的上下文测试,并找到匹配项,使用键路径字符串比较来确定特定的更改。 如果您为每个键路径提供了唯一的上下文,如下所示,一系列简单的指针比较可以同时告知您该通知是否适用于此观察者,如果是,则键路径已更改。

在任何情况下,观察者应该总是调用超类的实现observeValueForKeyPath:ofObject:change:context:当它不识别上下文(或在简单的情况下,任何键路径)时,因为这意味着超类也已经注册通知。

注意:如果通知传播到类层次结构的顶部,NSObject将抛出一个NSInternalInconsistencyException,因为这是一个编程错误:一个子类不能使用它注册的通知。


观察者的移除

通过向被观察对象发送消息removeObserver:forKeyPath:context:,指定观察对象、键路径和上下文,从而移除键值观察对象,下面列出的是Person对象移除自己作为balanceinterestRate的观察者。

- (void)unregisterAsObserverForAccount:(Account*)account 
{
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

接收到消息removeObserver:forKeyPath:context:之后,观察者不再收到任何指定的键和对象的observeValueForKeyPath:ofObject:change:context:消息。

当移除观察者时,还需要谨记几个问题:

  • 如果你在移除观察者时,这个观察者尚未注册,就会导致NSRangeException错误,相对于方法addObserver:forKeyPath:options:context:,你可以调用方法removeObserver:forKeyPath:context:一次,或者如果这个在你的app里面是不可行的,那么就在try/catch block里面处理这个潜在的错误和例外。

  • dellocated自己时,观察者不会自动移除自己。 观察到的对象继续发送通知,忽视观察者的状态。 但是,像发送给已释放对象的任何其他消息一样,更改通知会触发内存访问异常。 因此,您可以确保观察者在消失之前消除自己。

  • 如果该对象是观察者或观察者,则该协议无法询问对象。 构建您的代码以避免出现相关的错误。 典型的模式是在观察者初始化期间(例如在initviewDidLoad中)注册为观察者,并在释放(通常在dealloc中)时注销,确保正确配对和有序添加和删除消息,并且观察者要在自己在内存中释放之前移除自己作为其他属性或对象的观察者。

后记

未完,待续~~~

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

推荐阅读更多精彩内容