如何判断一个对象是否已经附加了键值观察器

143
如果你让一个 Objective-C 对象去removeObservers:某个键路径,而该键路径尚未注册,它会出现以下报错信息,如下所示:

Cannot remove an observer <observerObject> for the key path "theKeyPath" from <objectbeingObserved> because it is not registered as an observer.

是否有一种方法可以确定对象是否具有已注册的观察者,以便我可以执行此操作?
if (object has observer){
  remove observer
}
else{
  go on my merry way
}

我碰到了一个场景,更新 iOS 8 上的一个旧应用程序时,一个视图控制器被释放并抛出“无法移除”的异常。我认为在 viewWillAppear: 中调用 addObserver: 并相应地在 viewWillDisappear: 中调用 removeObserver: 就可以正确匹配这些调用。我必须快速修复,所以我将实现 try-catch 解决方案并留下评论以进一步调查原因。 - bneely
我正在处理类似的问题,我发现需要更深入地研究我的设计,并进行调整,以便不再需要移除观察者。 - Bogdan
在编程中,像这个答案建议的那样使用布尔值对我来说效果最好:https://dev59.com/vpfga4cB1Zd3GeqPALtR#37641685 - Lance Samaria
11个回答

317

在你的removeObserver调用周围放置一个try catch块

@try{
   [someObject removeObserver:someObserver forKeyPath:somePath];
}@catch(id anException){
   //do nothing, obviously it wasn't attached because an exception was thrown
}

12
1+ 好答案,对我有用,我也同意你在编辑之前的抱怨。 - Robert
25
因为已被删除的愤怒言论,我很可能会赞同,所以我点了个赞。 - Ben Gotow
12
还有没有其他更优雅的解决方案?这个需要至少2毫秒才能使用... 想象一下在tableviewcell中的情况。 - João Nunes
19
因为你没有提到这对于生产代码是不安全的,而且很可能随时失败,所以被踩了。在Cocoa中,通过框架代码引发异常不是一个选项。 - Nikolai Ruhe
6
如何在Swift 2.1中使用此代码。 尝试执行以下操作:do { try self.playerItem?.removeObserver(self, forKeyPath: "status") } catch let error as NSError { print(error.localizedDescription) }出现警告。 - Vipulk617
显示剩余8条评论

37

真正的问题是为什么你不知道自己是否正在观察它。

如果你是在所观察对象的类中这样做,请停止。任何观察者都希望继续观察它。如果你未经观察者同意就切断了观察者的通知,那就会导致问题;更具体地说,由于未接收到来自以前观察的对象的更新,预计观察者的状态会变得陈旧。

如果你是在观察对象的类中这样做,只需记住你正在观察哪些对象(或者,如果你只观察一个对象,则记住你是否正在观察它)。这是假设观察是动态的且处于两个不相关的对象之间的情况; 如果观察者拥有被观察者,则在创建或保留被观察者后添加观察者,在释放被观察者之前删除观察者。

将对象添加或删除为观察者通常应该发生在观察者的类中,而不是被观察对象的类中。


14
场景:您想在viewDidUnload和dealloc中删除观察者。这样会导致重复删除,如果您的视图控制器从内存警告中卸载,然后再释放,就会抛出异常。您如何建议处理这种情况? 建议如下:在dealloc方法中仅删除观察者一次,并将其从viewDidUnload中移除。当视图控制器被卸载时,系统会自动调用viewDidUnload方法并取消观察者。如果您的视图控制器被释放,则会将其从内存中移除,这不会产生任何问题。 - bandejapaisa
2
@bandejapaisa:基本上就是我在答案中说的:要跟踪我是否正在观察,并且只有在我正在观察时才尝试停止观察。 - Peter Hosey
42
不,那不是一个有趣的问题。你不应该需要追踪它;你应该能够在dealloc中简单地取消注册所有侦听器,而不必关心是否发生了添加代码路径。它应该像NSNotificationCenter的removeObserver一样工作,它不关心你是否实际上有监听者。这个异常只是在没有bug的情况下创建bug,这是糟糕的API设计。 - Glenn Maynard
1
@GlennMaynard:就像我在答案中所说的,“如果你在不让观察者知道的情况下切断它的通知,那么就应该预料到会出现问题;更具体地说,预计观察者的状态会变得陈旧,因为它无法从曾经被观察的对象接收更新。”每个观察者都应该结束自己的观察;未能这样做的话,最好是高度可见的。 - Peter Hosey
3
问题中没有提到移除其他代码的观察者。 - Glenn Maynard
显示剩余11条评论

25

值得一提的是,[someObject observationInfo]似乎在someObject没有任何观察者时会返回nil。然而,我不认为可以完全信赖这种行为,因为我没有看到官方文件中有关于这点的说明。此外,我也不知道如何读取observationInfo以获取特定的观察者。


你知道我怎么才能检索到一个特定的观察者吗?objectAtIndex:方法不能得到期望的结果。 - Eimantas
1
@MattDiPasquale 你知道我怎样才能在代码中读取observationInfo吗?它在打印时正常输出,但它是一个指向void的指针。我该怎么读取它? - neeraj
observationInfo是Xcode调试文档中记录的调试方法(标题中似乎有“magic”一词)。你可以尝试查找它。我可以告诉你,如果你需要知道是否有人正在观察你的对象 - 你正在做错事情。重新思考你的架构和逻辑。我是通过吃亏才学到这个教训的。) - Eimantas
源代码:NSKeyValueObserving.h - nefarianblack
加1分,因为这是一个滑稽的死胡同答案,但仍然有些有帮助。 - Will Von Ullrich
十年后... - neeraj

4

当您将观察者添加到对象中时,可以像这样将其添加到NSMutableArray中:

- (void)addObservedObject:(id)object {
    if (![_observedObjects containsObject:object]) {
        [_observedObjects addObject:object];
    }
}

如果您想取消对该对象的观察,可以执行以下操作:

for (id object in _observedObjects) {
    if ([object isKindOfClass:[MyClass class]]) {
        MyClass *myObject = (MyClass *)object;
        [self unobserveMethod:myObject];
    }
}
[_observedObjects removeAllObjects];

请记住,如果您取消观察单个对象,请从_observedObjects数组中删除它:
- (void)removeObservedObject:(id)object {
    if ([_observedObjects containsObject:object]) {
        [_observedObjects removeObject:object];
    }
}

1
如果在多线程环境中发生这种情况,您需要确保您的数组是线程安全的。 - shrutim
你正在保留一个对象的强引用,每次将对象添加到列表中都会增加其保留计数,并且除非从数组中删除其引用,否则它不会被释放。我建议使用 NSHashTable/NSMapTable 来保留弱引用。 - atulkhatri

4
唯一的方法是在添加观察者时设置一个标志。

3
最好创建一个KVO包装对象来处理添加和移除观察者,这样你就不必到处使用BOOL了。它可以确保观察者只被移除一次。我们就是使用类似的对象,并且它很有效。 - bandejapaisa
如果你不总是在观察,这是一个很棒的想法。 - Andre Simon

3

[someObject observationInfo] 如果没有观察者,返回nil

if ([tableMessage observationInfo] == nil)
{
   NSLog(@"add your observer");
}
else
{
  NSLog(@"remove your observer");

}

根据苹果文档:observationInfo返回一个指针,用于标识所有已向接收器注册的观察者的信息。 - FredericK
这个问题在@mattdipasquale的回答中表述得更好。 - Ky -

3

In my opinion - this works similar to retainCount mechanism. You can't be sure that at the current moment you have your observer. Even if you check: self.observationInfo - you can't know for sure that you will have/won't have observers in future.

Like retainCount. Maybe the observationInfo method is not exactly that kind of useless, but I only use it in debug purposes.

So as a result - you just have to do it like in memory management. If you added an observer - just remove it when you don't need it. Like using viewWillAppear/viewWillDisappear etc. methods. E.g:

-(void) viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self addObserver:nil forKeyPath:@"" options:NSKeyValueObservingOptionNew context:nil];
}

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [self removeObserver:nil forKeyPath:@""];
}

And it you need some specific checks - implement your own class that handles an array of observers and use it for your checks.


[self removeObserver:nil forKeyPath:@""];needs to go before:[super viewWillDisappear:animated]; - Joshua Hart
@JoshuaHart 为什么? - quarezz
因为这是一个拆解方法(dealloc)。当你重写某种拆解方法时,你最后调用super。例如:- (void) setupSomething { [super setupSomething]; … } - (void) tearDownSomething { … [super tearDownSomething]; } - Joshua Hart
viewWillDisappear 不是一个拆除方法,也与dealloc没有关联。如果您向导航栏推进,viewWillDisappear 将被调用,但您的视图将仍保留在内存中。我明白您的设置/拆除逻辑,但在此处执行它将不会带来任何实际好处。只有在基类中有一些可能与当前观察者发生冲突的逻辑时,您才需要在super之前放置删除。 - quarezz

2
观察者模式的整个重点在于允许被观察的类“封装”——不知道或不关心它是否正在被观察。您明确地试图打破这种模式。
为什么?
您遇到的问题是,您假定自己正在被观察,但实际上并非如此。这个对象没有开始观察。如果您希望您的类控制此过程,则应考虑使用通知中心。这样,您的类可以完全控制何时可以观察数据。因此,它不关心谁在观察。

11
他正在询问听众如何确定自己正在听到某些东西,而不是被观察的对象如何确定自己是否被观察。 - Glenn Maynard

1

我不太喜欢使用 try catch 解决方案,所以我大多数时候会在类内创建一个特定通知的订阅和取消订阅方法。例如,这两个方法可以将对象订阅或取消订阅全局键盘通知:

@interface ObjectA : NSObject
-(void)subscribeToKeyboardNotifications;
-(void)unsubscribeToKeyboardNotifications;
@end

在这些方法中,我使用一个私有属性,根据订阅状态设置为true或false,如下所示:
@interface ObjectA()
@property (nonatomic,assign) BOOL subscribedToKeyboardNotification
@end

@implementation

-(void)subscribeToKeyboardNotifications {
    if (!self.subscribedToKeyboardNotification) {
        [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(onKeyboardShow:) name:UIKeyboardWillShowNotification object:nil];
        [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(onKeyboardHide:) name:UIKeyboardWillHideNotification object:nil];
        self.subscribedToKeyboardNotification = YES;
    }
}

-(void)unsubscribeToKeyboardNotifications {
    if (self.subscribedToKeyboardNotification) {
        [[NSNotificationCenter defaultCenter]removeObserver:self name:UIKeyboardWillShowNotification object:nil];
        [[NSNotificationCenter defaultCenter]removeObserver:self name:UIKeyboardWillHideNotification object:nil];
        self.subscribedToKeyboardNotification = NO;
    }
}
@end

0

长话短说

这里获取NSObject类别,并像这样使用它:

if ([observable tdw_hasObserver:observer forKeyPath:@"key.path" context:nil error:nil]) {
    [observable removeObserver:observer forKeyPath:@"key.path"];
} else {
    // go on your merry way
}

公共API方法

对于许多没有其他观察者的Foundation类,您可以只检查observationInfo属性值。当对象没有任何观察者时,该属性返回空指针;否则返回不透明的void *指针:

if (observable.observationInfo) {
    [observable removeObserver:observer forKeyPath:@"key.path"];
} else {
    // go on your merry way
}

然而,这并不适用于大多数UIKit类和情况,其中您自己使用了多个观察者。


私有 API 方法

如果您不介意使用私有 API,任何对象都可以通过使用 NSObjectobservationInfo 属性向您提供指向其观察者数据的不透明指针。在幕后,该指针保存一个对象,该对象又保存了所谓的观察数组 - 一种特殊的数据结构,用于描述单个订阅(每次调用 -[NSObject addObserver:forKeyPath:options:context:] 都会在数组中创建一个新实例,即使所有参数都相同)。观察内存布局(至少在撰写本答案时)如下:

@interface NSKeyValueObservance: NSObject {
    id _observer;
    NSKeyValueProperty *_property;
    void *_context;
    id originalObservable;
}

这些变量的组合是您要查找的信息。当然,由于它是私有API,您无法可靠地提取此数据,但以下契约自古以来一直在运作:

  1. observationInfo 应该有一个 NSArray 实例变量,其中包含观察数据;
  2. _observer_property_context 实例变量名称不变;
  3. _context_observer 可以通过指针地址与所需数据进行比较;
  4. _property 实例变量具有类型为 NSString_keyPath,用于与所需的键路径进行比较;

有了这个想法,您可以使用类别扩展 NSObject 并实现一个方便的方法,检查是否存在某种组合。

我不介意分享我的实现,但它太多了,无法在一个SO答案中适合。您可以在我的代码片段页面上查看。 请注意,此实现通过名称和(有时)类型执行ivars查找。 它比直接使用ivar偏移量更安全,但仍然非常脆弱。

这里是如何使用它的简单示例:

NSObject *observable = [NSObject new];
NSObject *observer = [NSObject new];
void *observerContext = &observerContext;

[observable addObserver:observer forKeyPath:@"observationInfo" options:NSKeyValueObservingOptionNew context:observerContext];
[observable addObserver:observer forKeyPath:@"observationInfo" options:NSKeyValueObservingOptionNew context:nil];
[observable addObserver:observer forKeyPath:@"observationInfo" options:NSKeyValueObservingOptionNew context:observerContext];

// Removes only 2 observances (subscriptions) where all parts matches (context, keyPath and observer instance)
while ([observable tdw_hasObserver:observer forKeyPath:@"observationInfo" context:observerContext error:nil]) {
    [observable removeObserver:observer forKeyPath:@"observationInfo" context:observerContext];
}

两句话的文档说明

如果context和/或keyPath参数为nil,则实现将查找具有任何context和/或keyPath的订阅。

在任何(预期的)错误情况下,该方法将返回NO并将错误详细信息写入error对象(如果提供了相应的指针)。 预期的错误包括私有API中的一些微小更改(但远非全部)。


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