addObserver (KVO) 中上下文参数的最佳实践

45

当您观察属性时,我想知道在KVO中应该设置什么上下文指针。我刚开始使用 KVO,从文档中并没有学到太多的东西。我在这个页面上看到: http://www.jakeri.net/2009/12/custom-callout-bubble-in-mkmapview-final-solution/ 作者做了这个:

[annView addObserver:self
forKeyPath:@"selected"
options:NSKeyValueObservingOptionNew
context:GMAP_ANNOTATION_SELECTED];

然后在回调函数中,执行这个操作:

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context{

NSString *action = (NSString*)context;


if([action isEqualToString:GMAP_ANNOTATION_SELECTED]){
我假设在这种情况下,作者只是创建一个字符串以后在回调函数中进行识别。然后我在iOS 5 Pushing the Limits这本书中看到他这样做:
[self.target addObserf:self forKeyPath:self.property options:0 context:(__bridge void *)self];

回调函数:

if ((__bridge id)context == self) {
}
else {
   [super observeValueForKeyPath .......];
}

我想知道在传入上下文指针时是否有标准或最佳实践?

3个回答

109

重要的是(一般而言)您使用某些东西(而不是什么都不用),并且您使用的任何东西都必须独特并且专为您的使用而设置

主要的陷阱在于当您在其中一个类中有一个观察时,然后有人对您的类进行子类化,并添加了相同观察对象和相同keyPath的另一个观察。如果您最初的observeValueForKeyPath:...实现仅检查了keyPath或观察到的object,甚至是两者,那可能不足以知道它是正在调用的观察。使用值对您唯一且私有的context可以让您更加确信给定的observeValueForKeyPath:...调用就是您期望的调用。

例如,如果您仅注册了didChange通知,但是子类使用NSKeyValueObservingOptionPrior选项注册相同对象和keyPath,则会产生影响。如果您没有使用context(或检查更改字典)过滤对observeValueForKeyPath:...的调用,则处理程序将多次执行,而您只希望执行一次。不难想象这可能会引起问题。

我使用的模式是:

static void * const MyClassKVOContext = (void*)&MyClassKVOContext;

这个指针将指向它自己的位置,而且那个位置是唯一的(没有其他静态或全局变量可以有这个地址,也没有任何堆栈分配的对象可以有这个地址--虽然这不是绝对的保证,但非常强),得益于链接器。 const 使得编译器会警告我们如果我们试图编写更改指针值的代码,并且最后,static 使它对本文件私有,因此在该文件外部无法获得对它的引用(再次使其更容易避免冲突)。

我特别警告反对使用的一个模式是出现在问题中的模式:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    NSString *action = (NSString*)context;
    if([action isEqualToString:GMAP_ANNOTATION_SELECTED]) {

context被声明为void*,这意味着这是关于它所表示的所有保证。通过将其强制转换为NSString*,您正在打开一个潜在的严重问题。如果其他人恰好使用了不是 NSString* 的类型作为context参数,那么当您将非对象值传递给isEqualToString:时,这种方法就会崩溃。指针相等性(或者选择intptr_tuintptr_t相等性)是唯一可用于context值的安全检查。

使用self作为context是一种常见的方法。虽然比没有强大,但具有更弱的唯一性和隐私保护,因为其他对象(更不用说子类)可以访问self的值并将其用作context(导致歧义),而上面我建议的方法则不同。

还要记住,可能会引起问题的不仅是子类;虽然这可能是一种罕见的模式,但没有任何阻止另一个对象为您的对象注册新的KVO观察的东西。

为了提高可读性,您还可以将其包装在预处理器宏中,如下所示:

#define MyKVOContext(A) static void * const A = (void*)&A;

5
这篇小论文写得真是不错,我很惊讶原作者还没有接受它。 - matt
4
KVO 将上下文视为整数。它不关心它“指向”什么,因此如前所述,使其指向自身可以实现良好的唯一性(即后来可能会有一个动态分配的对象具有相同的地址)。 - ipmcc
一个更有趣的不使用self作为上下文的原因是,self会被子类和父类中的代码共享。例如,如果您有一个超类Person,一个名为Employee的子类以及一个名为Manager的子类,它们都将使用相同的self。它们可能不应该共享上下文,否则Manager中的未来更改可能会影响Person - Steven Fisher
这正是我所说的“私人使用”的意思。 - ipmcc
1
self 作为一个指向对象的指针,基本上就是“不私有于它的使用”的定义。所以,是的,有很多理由不要将其用作 KVO 上下文。 - ipmcc
显示剩余4条评论

19
KVO上下文应该是指向静态变量的指针,就像这个代码片段所展示的那样。通常我会这样做:

在我的文件ClassName.m的顶部附近,我会加入以下行:

static char ClassNameKVOContext = 0;

当我开始观察targetObject(一个TargetClass实例)上的aspect属性时,我会有

[targetObject addObserver:self
               forKeyPath:PFXKeyTargetClassAspect
                  options://...
                  context:&ClassNameKVOContext];

TargetClass.m 中定义了一个名为 PFXKeyTargetClassAspectNSString * 类型变量,其值等于 @"aspect",并在 TargetClass.h 中被声明为 extern。这里的“PFX”当然只是你项目中使用的前缀的占位符。这样做有助于自动补全并防止出现拼写错误。

当我完成对 targetObject 上的 aspect 的观察时,我将得到:

[targetObject removeObserver:self
                  forKeyPath:PFXKeyTargetClassAspect
                     context:&ClassNameKVOContext];

为了避免在我的 -observeValueForKeyPath:ofObject:change:context: 实现中过多地缩进,我喜欢编写

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context != &ClassNameKVOContext) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        return;
    }

    if ([object isEqual:targetObject]) {
        if ([keyPath isEqualToString:PFXKeyTargetClassAspect]) {
            //targetObject has changed the value for the key @"aspect".
            //do something about it
        }
    }
}

嗨,我正在尝试为-observeValueForKeyPath:ofObject:change:context:中的处理方式添加单元测试。我的问题是,我正在观察一个对象上的只读属性,而我无法在单元测试中更改该对象以触发KVO,因此我需要直接在单元测试中手动调用-observeValueForKeyPath:ofObject:change:context:方法。但是,当我传递并传递上下文时,它总是不同的,我的单元测试总是失败。您有任何想法如何传递在单元测试中相同的上下文吗? - Daniel Sanchez
我不同意这种模式。KVO上下文并不是用于识别类,而是用于识别当任何数量的关键路径更改时应该发生的操作。例如,名称和年龄关键路径都应该导致updateView被调用。使用上下文指针可以快速完成此操作,而无需进行关键路径字符串比较。 - malhal

4

我认为更好的实现方式是按照苹果文档所说:

在您的类中,唯一命名的静态变量的地址是一个很好的上下文。

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

请查看文档了解更多相关技术。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接