Objective-C:KVO

      KVO(键值观察)是Objective-C提供的一种观察对象属性变化的机制,其内部是利用KVC技术来实现观察者设计模型。利用KVO用户可以注册一个对象为另一个对象的观察者,并在被观察对象的属性发生变化时能收到通知。

1 使用KVO

      利用键值观察(Key Value Observing),可以自动观察其他对象的变化。因此,当一个对象改变状态(属性)时,其它对象就会得到通知。通过键值观察,你不需要手动告诉其他对象进行更新。它们会自动收到新值并执行适当的操作。这极其强大。设置是该技术最强大的应用之一,此外, Cocoa 框架中的核心数据和其他技术也利用了键值观察实现了一些奇妙的功能。

     要使用KVO需要进行一些操作和配置:

    1) 要使用键值观察,被观察的对象必须对所观察的特性(属性)使用符合 KVC 标准的存取器方法。

    2) 想要观察变化的对象,也就是观察者,必须实现一个接收通知的回调方法。该方法是-observeValue:forKeyPath:ofObject:change:context:。该方法在值变化时被调用并可以配置成同时接收新值和原值以及其他自定义的信息。

    3) 观察者通过调用-addObserver:forKeyPath:options:context:方法针对被观察对象进行注册。调用该方法,告诉对象要观察的 KVC 键路径以及希望看到的变化,并提供一个在收到变化通知时可以传回的上下文对象。

      注意观察者完成这些配置后,键路径指定的属性的任何变化都可以自动调用观察者的回调方法。在观察者完成对被观察对象的观察后,必须将自己移除。如果没有做到这点并且观察者之后就释放了,将来向该观察者发送通知时可能会导致应用崩溃。

2 注册成为观察者

      注册成为观察者很容易。针对想要观察的对象简单调用-addObserver:forKeyPath:options:context:方法,

1 [obj addObserver:self
2 forKeyPath:@”memberVariable”
3 options:(NSKeyValueObservingOptionNiew | NSKeyValueObservingOptionOld)
4 context:NULL];

      Observer 参数通常是 self,这是在被观察值变化时收到通知的对象。键路径参数指定想要观察的特性的键路径。 options 参数指定一些标记来告诉 KVO 你希望变化如何传给你。这些值可以通过|操作符进行或操作。传入的可能值如表 21所示。

表 21 传入的可能值

功 能

NSKeyValueObservingOptionNew

作为变更信息的一部分发送新值

NSKeyValueObservingOptionOld

作为变更信息的一部分发送旧值

NSKeyValueObservingOptionInitial

在观察者注册时发送一个初始更新

NSKeyValueObservingOptionPrior

在变更前后分别发送变更,而不只在变更后发送一次

      上下文参数是一个在 KVO系统中无变更传递的void*参数,即当被观察的属性发生变化时,会被传回给观测对象的回调方法。本质上,就 KVO 而言,该参数是不透明的数据块,任何从此传入参数的都会无变更传递的。

注意:

       记住使用 void*上下文参数时有和垃圾回收相关的特殊规则,你必须确保 void*指向的数据在之后访问时仍然没有被释放并有效。换句话说,不要将一些存储在栈上的值传递给该参数,这会导致崩溃。

3 实现KVO的回调方法

      使用 KVO 的第二步就是编写观察者的回调方法。如下的实现代码显示了-observeValue:forKeyPath:ofObject:change:context:方法的一个示例实现。

 1 -(void)observeValuseForKeyPath: (NSString*)inKeyPath
 2                       ofObject: (id)inObject
 3                         change: (NSDictionary*)inChange
 4                        context: (void*)inCtx
 5 {
 6       if(inKeyPath isEqualToString:@”memverVariable”)
 7       {
 8             NSString *newValue = [inChange objectForKey:NSKeyValuseChangeNewKey];
 9       }
10       else if([inKeyPath isEqualToString:@”…”])
11      {
12           …
13      }
14 }

      可以从该方法看出,要做的第一件事情就是找出被观察对象中变化的特性。该方法自动传入一个对象参数,告诉你哪个对象向你发送通知。通过对键路径的传入值使用-isEquals 方法,你可以准确地确定对象的什么特性发生了改变。 Key 参数仅仅是一个字符串,和对 KVC 使用时一样。因此,可以使用 NSString 方法-isEqualToString:来确定该通知所对应的键路径。

      在确定了对象的哪个特性发生变化后,你可以执行任何合适的操作。实际的变化通过 change参数传递给你。该参数是一个 NSDictionary 对象,包括和你注册成为观察者时所请求的变化信息相关的键和值。这些键和值如表 22所示。

表 22 和变化信息相关的键值

NSKeyValueChangeKindKey

指定变化类型的 NSNumber

NSKeyValueChangeNewKey

新值

NSKeyValueChangeOldKey

原值

NSKeyValueChangeIndexeskey

如果 NSKeyValueChangeKindKey 是 NSKeyValueChangeInsertion、 NSKeyValueChangeRemoval、NSKeyValueChangeReplacement 之一,该值就包含变化值的索引

NSKeyValueChangeNotificationIsPriorKey

和 NSKeyValueChangeOptionPrior 结合使用来表示这是"之前"的通知

      可以看出,如果选择同时接收原值和新值,两个都会在变化参数中提供,通过合适的键就可以访问到。从变化字典中获取到值之后,就可以在对象中使用它执行任何需要的操作。

      记住 KVC 必须使用对象来发送值——不能直接使用标量和结构体。因此,如果所观察的值是标量或者结构体,所接收的值就分别是 NSNumber 或 NSValue 类型的。因此,必须从该值中提取出实际需要的标量或者结构体值。上述示例代码就展示了这一点。

NSKeyValueChangeKindKey 指定了接收到的变化类型。可能的类型如表 23所示。

表 23 可能的变化类型

功 能

NSKeyValueChangeSetting

指定该值被设置

NSKeyValueChangeInsertion

指定这些值插入到集合或者一对多的关系中

NSKeyValueChangeRemoval

指定这些值在一对多的关系中被移除

NSKeyValueChangeReplacement

指定这些值在一对多的关系中被替换

4 移除观测者

       记住在结束对一个对象变化的观察后,需要移除观察者。如果不这样,应用可能会崩溃。

为了移除观察者,只需要调用方法-removeObserver:forKeyPath:,并传入观察者作为第一个参数,观察的键路径是第二个参数。代码清单 6-16 显示了一个在观察者的 dealloc 方法中实现的示例。

1 -(void) dealloc:
2 {
3      [obj removeObserver: self forKeyPath:@”memberVariable”];
4      [super dealloc];
5 }

说明:

       在垃圾回收的环境中,如果忘记移除观察者可能不会造成崩溃。但是,移除观察者仍是一种好的做法,这样就可以在不支持垃圾回收的环境中形成该习惯。

5 实现手动通知

      所有的这些通知都自动发生。需要做的就是为属性提供符合 KVC 标准的存取器方法,其他一切都会正常工作。有时,不一定想利用自动通知。有时想在改变一个值或者一组值时手动发送通知。比如,如果需要一次性进行很多变更,可能会想将这些变化打包后一起发送。在这些情况下就会使用手动通知。

      为了实现手动通知,必须重写类方法+automaticallyNotifiesObserversForKey:来告诉 Objective-C 你不想自动通知观察者所发生的变化。可以通过对任意一个想进行手动通知的键返回 NO 来实现。示例如代码清单 6-17 所示。

1 +(BOOL)automaticallyNotifiesObserversForKey:(NSString*)inKey
2 {
3      if([inKey isEqualToString:@”memberVariable”])
4          return NO;
5      return YES;
6 }

       如果想要手动通知所发生的变化,你必须在变化之前调用方法-willChangeValueForKey:,然后在变化之后调用方法-didChangeValueForKey:。示例如代码清单 6-18 所示。

1 -(void)setMemberVariable:(CGFloat)inValue
2 {
3     [self willChangeValueForKey:@”memberVariable”];
4     memberVariable = inValue;
5     [self didChangeValueForKey:@”memberVariable”];
6 }

       这些调用在需要时是可以嵌套的,比如在一次调用中需要修改多个变量的情况。此外还有和一对多关系对应的调用。它们是-willChange:valuesAtIndexes:forKey:和-didChange:valuesForIndexes:forKey:。

6 使用KVO的风险

      使用 KVO 也会遇到问题,更具体点说,使用 KVO 最大的风险就是如果观察者观察每一步,这些观察者可能会执行其他操作,因为你无法控制这些观察者,所以也就无法控制这些操作。

      大多数情况下这不会成为一个问题,但也在例外。这种情况就是在初始化函数或者 dealloc函数中使用存取器方法来释放变量成员时,如代码清单 6-19 所示。

1 -(void)dealloc
2 {
3      [self setFoo:nil];
4      [self setBar:nil];
5      [super dealloc];
6 }

       按这种方式写 dealloc 方法很好!可以在释放成员变量的同时将它设成nil,一步搞定。问题就是,在调用这些存取器方法时, KVO 观察者会在这些变化发生时收到通知。如果他们不想接收 nil 或者希望在收到通知时能够处理对象本身,此时就会发生一些糟糕的事情。此外,如果想到了观察者在收到 bar 变量的变化通知时,希望可以访问 foo 变量,这种情况下就会造成一个问题,因为 foo 变量已经被释放并且被设置成 nil。

       苹果目前推荐的做法就是不要在初始化函数或者 dealloc 方法中通过存取器方法初始化和释放成员变量。这种情况在 64 位运行时中变得更复杂,因为它可以在没有相关的成员变量的情况下声明属性。在这些情况中,初始化和释放成员变量的唯一办法就是通过存取器方法。

我是使用存取器方法来初始化和释放成员变量的,除非我知道在给定的环境中这样做会造成问题。此外,实现键值观察者时,我会确保观测者可以正确处理 nil 值并尽量最小化副作用。

      如果你觉得这种风险是值得的,那就通过存取器方法来编写初始化函数和释放函数吧。只要意识到可能的危险,在遇到问题时,就可以马上知道应该从哪里查找。另一方面,如果你不能确保观察者会这么做的话,那就就遵循苹果的建议,除非不得已,否则不要在初始化函数和析构函数中使用存取器方法。

原文地址:https://www.cnblogs.com/huliangwen/p/5462894.html