【问题标题】:How to debug KVO如何调试 KVO
【发布时间】:2011-11-11 11:16:38
【问题描述】:

在我的程序中,我手动使用 KVO 来观察对象属性值的变化。我在自定义设置器中的以下代码行收到EXC_BAD_ACCESS 信号:

[self willChangeValueForKey:@"mykey"];

奇怪的是,当工厂方法调用自定义设置器并且周围不应该有任何观察者时,就会发生这种情况。我不知道如何调试这种情况。

更新:列出所有注册观察者的方式是observationInfo。事实证明,确实列出了一个指向无效地址的对象。但是,我完全不知道它是如何到达那里的。

更新 2: 显然,可以为给定对象多次注册相同的对象和方法回调 - 导致观察对象的 observationInfo 中的条目相同。删除注册时,仅删除其中一个条目。这种行为有点违反直觉(添加多个条目肯定是我的程序中的一个错误),但这并不能解释虚假观察者如何神秘地出现在新分配的对象中(除非有一些缓存/重用正在发生,我不知道)。

修改后的问题:我怎样才能确定对象在何处以及何时注册为观察者?

更新 3: 具体示例代码。

ContentObj 是一个类,它有一个字典作为名为@9​​87654326@ 的属性。它覆盖:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"mykey"]) {
        automatic = NO;
    } else {
        automatic=[super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

一些属性具有如下的 getter 和 setter:

- (CGFloat)value {
    return [[[self mykey] objectForKey:@"value"] floatValue];
}
- (void)setValue:(CGFloat)aValue {
    [self willChangeValueForKey:@"mykey"];
    [[self mykey] setObject:[NSNumber numberWithFloat:aValue]
                     forKey:@"value"];
    [self didChangeValueForKey:@"mykey"];
}

容器类具有类NSMutableArray 的属性contents,它包含类ContentObj 的实例。它有几个手动处理注册的方法:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"contents"]) {
        automatic = NO;
    } else {
        automatic=[super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

- (void)observeContent:(ContentObj *)cObj {
    [cObj addObserver:self
           forKeyPath:@"mykey"
              options:0
              context:NULL];
}

- (void)removeObserveContent:(ContentObj *)cObj {
    [cObj removeObserver:self
              forKeyPath:@"mykey"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    if (([keyPath isEqualToString:@"mykey"]) &&
        ([object isKindOfClass:[ContentObj class]])) {
        [self willChangeValueForKey:@"contents"];
        [self didChangeValueForKey:@"contents"];
    }
}

容器类中有几个方法可以修改contents。它们如下所示:

- (void)addContent:(ContentObj *)cObj {
    [self willChangeValueForKey:@"contents"];
    [self observeDatum:cObj];
    [[self contents] addObject:cObj];
    [self didChangeValueForKey:@"contents"];
}

还有其他几个提供与数组类似的功能。他们都通过添加/删除自己作为观察者来工作。显然,导致多次注册的任何事情都是一个错误,并且可能隐藏在这些方法中某处

我的问题针对如何调试这种情况的策略。或者,请随时提供实现这种通知/观察者模式的替代策略。

更新 4: 我使用断点、NSLogs、代码审查和出汗的混合方式发现了这个错误。我没有在 KVO 中使用上下文,尽管这绝对是另一个有用的建议。这确实是一个错误的双重注册——由于我无法理解的原因——导致了观察到的行为。

包括[self willChange...]; [self didChange...] 在内的实现如所描述的那样工作(在iOS 5 上),尽管它远非漂亮。问题在于NSArray 不符合 KVO,因此无法谈论对其内容的更改。我也考虑过 Mike Ash 建议的通知,但我决定使用 KVO,因为这似乎更像是一种Cocoa-ish 机制来完成这项工作。这可以说不是最好的决定......

【问题讨论】:

  • 一些代码会很有用.. 你在哪里添加你的观察者?为什么要发送自定义通知? myKey 的 getter 和 setter 是什么样的?你在用 Zombies 工具来查看它崩溃的地方吗?
  • @hooleyhoop:我正在发送自定义通知,因为我使用一对多关系(NSMutableArray)并且希望在 1)数组被修改(添加/删除对象)时收到通知/rearranged) 和 2) 数组内的对象被修改。由于 Cocoa Touch 没有NSArrayController,这似乎是唯一的可能性。
  • @hooleyhoop:我曾尝试使用 Zombies 工具,但它并没有太大帮助,因为问题与 KVO 有关,而不是直接与引用计数有关。我只观察到当我不使用 Zombies 时它会崩溃,而当我使用时它不会崩溃。 Zombies 无法帮助我识别导致崩溃的对象,因为崩溃与在 KVO 中注册的对象有关,而不是与 release 相关。
  • 嗯,您不需要手动触发任何通知来执行此操作,但这并不是说如果您这样做就会崩溃。您是否将观察者添加到数组中?子类化一个数组?很多问题.. 不看代码就无法回答。
  • 我现在已经发布了具体的代码。最初,我正在寻找有关如何调试此场景的策略。由于在数百行这样的代码中某处存在错误,我认为不太可能有人可以帮助我找到它(毕竟,这是我的工作!)。所以我主要是在确定工具和策略方面寻求帮助——比如 observationInfo 结构和我不知道的类似事情。

标签: iphone objective-c cocoa-touch key-value-observing


【解决方案1】:

是的,两次调用-addObserver: 将导致两次注册。 Foo 类和 Foo 的某个子类 Bar 可能都(合法地)注册相同的通知,但具有不同的上下文(始终包含上下文,始终检查 -observeValueForKeyPath 中的上下文并始终在 -observeValueForKeyPath 中调用 super)。

这意味着一个 Bar 的实例会注册两次,这是正确的。

但是,您几乎肯定不想意外地多次注册同一个对象/keypath/context,正如@wbyoung 所说,覆盖-addObserver:forKeyPath:options:context: 应该可以帮助您确保不会发生这种情况。如果需要在数组中跟踪观察者/keypath/context 并确保它们是唯一的。

Mike Ash has some interesting thoughts and code on his blog about using contexts。他说它被破坏是对的,但实际上 KVO 是完全可用的。

也就是说,当你用它来做某事时,它就意味着要做。以前是绝对不能做这种事的。。

[self willChangeValueForKey:@"contents"];
[self didChangeValueForKey:@"contents"];

因为这是一个谎言。调用-willChange.. 时的“contents”值必须与调用-didChange.. 时的值不同。 KVO 机制将在-willChangeValueForKey-didChangeValueForKey 中调用-valueForKey:@"contents" 来验证值是否已更改。这显然不适用于数组,因为无论您如何修改内容,您仍然拥有相同的对象。现在我不知道情况是否仍然如此(网络搜索什么也没找到)但请注意-willChangeValueForKey, -didChangeValueForKeyare not the correct way to handle manual kvo of a collection. Apple 提供了替代方法:-

– willChange:valuesAtIndexes:forKey:
– didChange:valuesAtIndexes:forKey:
– willChangeValueForKey:withSetMutation:usingObjects:
– didChangeValueForKey:withSetMutation:usingObjects:

值必须更改可能仍然不正确,但如果是,您的方案将无法正常工作。

我要做的就是收到一份通知,通知您对您的收藏进行修改。以及修改该集合中项目的不同通知。即,当您尝试触发@"contents" 的通知时,您可以拥有@"contents" 和@"propertiesOfContents"。您需要观察两个键路径,但您可以使用自动 kvo 而不是手动触发通知。 (使用自动 kvo 将确保调用正确版本的 -willChange.. -didChange..

对于数组的自动 kvo,请查看(不需要 NSArrayController):- Key-Value-Observing a to-many relationship in Cocoa

然后,每次将项目添加到集合中时,请观察您需要的属性(就像您现在所做的那样)并在它们更改时翻转 self.propertiesOfContents 的值。 (好吧,当我读回来时,它听起来并不一定比您的解决方案更老套,但我仍然相信它可能会表现得更好)。

【讨论】:

  • 感谢您的详细回答。您建议的方法(willChange:valuesAtIndexes:forKey: 等)需要重新设计,以使包含类完全从调用者那里抽象出集合。它不适用于链式通知或调用者能够直接访问数组中的元素时。下次我会更好地考虑到这一点,但现在我坚持我目前的设计......:^/
【解决方案2】:

针对您修改后的问题,尝试在您的自定义类中覆盖 addObserver:forKeyPath:options:context: 并在其上设置断点。或者,您可以在-[NSObject addObserver:forKeyPath:options:context:] 上设置一个符号断点,但这可能会受到很多影响。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多