【问题标题】:iOS - How to remove observer from singleton NSObject for KVO?iOS - 如何从 KVO 的单例 NSObject 中删除观察者?
【发布时间】:2013-03-15 04:33:37
【问题描述】:

我有一个共享的 NSObject 单例类,我在其中运行了一些操作队列。我遇到了崩溃:

[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];

看来我需要使用 'removeObserver:' 来防止这种情况发生,但是如何在共享对象上正确执行此操作?

代码:

-(void)synchronizeToDevice{
    queue = [NSOperationQueue new];
    queue.name = @"SynchronizeToDeviceQueue";
    //Sync Active User
    NSInvocationOperation *operationUser = [[NSInvocationOperation alloc] initWithTarget:self
                                                                                selector:@selector(downloadUserData:)
                                                                              object:[self activeUserID]];

    [queue addOperation:operationUser];

    //Sync Video Data
    NSInvocationOperation *operationVideos = [[NSInvocationOperation alloc] initWithTarget:self
                                                                            selector:@selector(downloadVideoData)
                                                                              object:nil];
    [queue addOperation:operationVideos];


    [queue addObserver:self forKeyPath:@"operations" options:0 context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == queue && [keyPath isEqualToString:@"operations"]) {
        //Synchronization Queue
        if ([queue.name isEqualToString:@"SynchronizeToDeviceQueue"] && [queue.operations count] == 0) {
            //Queue Completed
            //Notify View Synchronization Completed
            [self performSelectorOnMainThread:@selector(postNotificationDidFinishSynchronizationToDevice) withObject:nil waitUntilDone:NO];
        }
        //Video Download Queue
        if ([queue.name isEqualToString:@"VideoFileDownloadQueue"] && [queue.operations count] == 0) {
            //Notify View Video File Download Completed
            [self performSelectorOnMainThread:@selector(postNotificationDidFinishDownloadingVideo) withObject:nil waitUntilDone:NO];
        }
        //Active User Sync Queue
        if ([queue.name isEqualToString:@"SynchronizeActiveUserToDeviceQueue"] && [queue.operations count] == 0) {
            //Queue Completed
            //Notify View Synchronization Completed
            [self performSelectorOnMainThread:@selector(postNotificationDidFinishActiveUserSynchronizationToDevice) withObject:nil waitUntilDone:NO];
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

崩溃日志:

2013-03-14 21:48:42.167 COMPANY[1946:1103] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<DataManager: 0x1c54a420>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: operations
Observed object: <NSOperationQueue: 0x1c5d3360>{name = 'SynchronizeActiveUserToDeviceQueue'}
Change: {
    kind = 1;
}
Context: 0x0'
*** First throw call stack:
(0x336262a3 0x3b4b197f 0x336261c5 0x33f1a56d 0x21bd1 0x33eb46b9 0x33eb4313 0x33eb3a25 0x33eb3817 0x33f2b689 0x3b8ccb97 0x3b8cf139 0x3b8cd91d 0x3b8cdac1 0x3b8fda11 0x3b8fd8a4)
libc++abi.dylib: terminate called throwing an exception

【问题讨论】:

  • 你能发布崩溃日志吗
  • 什么代码,什么类,崩溃了?是什么让您相信移除观察者会阻止它?不幸的是,您的问题目前还不清楚。
  • 抱歉,添加了代码和崩溃日志。感谢您的帮助!

标签: ios singleton key-value-observing nsobject observers


【解决方案1】:

我怀疑您拨打synchronizeToDevice 的电话不止一次。如果是这样,您将继续观察旧队列以及一些新队列。当observeValueForKeyPath:... 触发时,它可能会将旧队列传递给您,然后您会忽略它,调用super,这会引发异常,因为您没有处理您要求的观察。

您真正的问题是您没有使用访问器。这会让事情变得更清楚。例如,这就是你将如何实现setQueue:

-(void)setQueue:(NSOperationQueue *)queue {
  if (_queue) {
    [_queue removeObserver:self forKeyPath:@"operations"];
  }

  _queue = queue;

  if (_queue) {
    [_queue addObserver:self forKeyPath:@"operations" options:0 context:NULL];
  }
}

现在,当您拨打self.queue = [NSOperationQueue new]; 时,一切都会自动运行。您停止观察旧队列并开始观察新队列。如果您致电self.queue = nil,它会自动为您取消注册。

您仍然需要确保在dealloc 中取消注册:

- (void)dealloc {
  if (_queue) {
    [_queue removeObserver:self forKeyPath:@"operations"];
  }
}

【讨论】:

  • 所以你是对的,每次打开popover都会调用synchronizeToDevice。因此弹出框的快速打开/关闭最终会导致崩溃。但是,我添加了您提供的自定义 setQueue 方法,但它仍然崩溃。我设置了断点以确保它被调用,但不确定它为什么仍然崩溃。有什么建议吗?
【解决方案2】:

在《Key-Value Observing Programming Guide》的Receiving Notification of a Change中,给出了observeValueForKeyPath的一个示例实现,附注:

确保调用超类的实现如果它实现了它。 NSObject 没有实现该方法。

你说你的类是NSObject的子类,所以你不应该调用[super observeValueForKeyPath:...]

如果您在同一个共享实例上多次调用synchronizeToDevice,则会出现另一个问题。在这种情况下,您创建一个新的queue 并为此注册一个观察者。但是旧队列的观察者并没有被移除。

因此,observeValueForKeyPath 可能会被称为“旧队列”并且检查 if (object == queue) 失败,导致对 super 的不必要调用。

所以如果synchronizeToDevice可以被多次调用,你应该先移除旧的观察者。

【讨论】:

  • 虽然真实且有帮助,但我认为这不是实际问题。错误告诉我们的是,KVO 回调是在没有人预料到的时候被调用的(没有人处理它)。只需删除对super 的调用即可掩盖该问题,但问题仍然存在。对super 的调用本质上就像NSAssert(I_SHOULD_HAVE_HANDLED_THAT)
  • @RobNapier:是的,这听起来很合理。
  • @RobNapier:我没有注意到你在我更新我的答案时发布了答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-16
  • 1970-01-01
  • 1970-01-01
  • 2018-03-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多