0

I'm registering for callbacks on for my NSOperationQueue like this:

[self.queue addObserver:self forKeyPath:@"operationCount" options:NSKeyValueObservingOptionNew context:NULL];

Because I have an expirationhandler for the long task, I do this in the callback of the operationCount. I basically am trying to save the state after an NSOperation in my queue finishes, and then resume it later. So I do this:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"operationCount"]) {
        NSNumber *num = [change objectForKey:NSKeyValueChangeNewKey];
        self.progress = (1.0 - (double)[num integerValue] / self.totalPackets);

        if ([UIApplication sharedApplication].backgroundTimeRemaining <= MIN_BACKGROUND_TIME) {
            // Background time is almost up, save the state and resume later
            NSLog(@"running out of time");
            [self.queue cancelAllOperations];
            [self.queue removeObserver:self forKeyPath:@"operationCount" context:NULL];

            if (self.patientProcessingTaskID != UIBackgroundTaskInvalid) {
                [[UIApplication sharedApplication] endBackgroundTask:self.patientProcessingTaskID];
                self.patientProcessingTaskID = UIBackgroundTaskInvalid;
            }
        }

        if (self.queue.operationCount == 0) {
            NSLog(@"no more operations");
            [self.queue removeObserver:self forKeyPath:@"operationCount" context:NULL];

            if (self.patientProcessingTaskID != UIBackgroundTaskInvalid) {
                [[UIApplication sharedApplication] endBackgroundTask:self.patientProcessingTaskID];
                self.patientProcessingTaskID = UIBackgroundTaskInvalid;
            }
        }
    }
}

It does not work as I expected. I step through the code and I see that [self.queue removeObserver:..] gets run. However, I still end up getting a callback in my obserValueForKeyPath: method which I'm not sure why (assuming that I removed myself as the observer for the self.queue. Am I removing self correctly? Thanks!

Crystal
  • 28,460
  • 62
  • 219
  • 393

1 Answers1

0

Are you sure you're only calling -addObserver:... once? If you call -addObserver:... multiple times, you'll get multiple callbacks to -observeValueForKeyPath:... and you'll have to call -removeObserver: once for every call to -addObserver:... in order to stop receiving callbacks.

I also see another problem here; In my experience, calling addObserver:... or removeObserver:... in observeValueForKeyPath:... (for the same keyPath) is a recipe for trouble. You've not posted crash traces with the "smoking gun" stack frame in them, but adding and removing observers during notifications for the same key path can cause intermittent crashes. I've empirically observed that the order in which observers of a given property are notified is non-deterministic, so even if adding or removing observers inside notification handlers works some of the time, it can arbitrarily fail under different circumstances. (I'm assuming there's an unordered data structure down in the guts of KVO somewhere that can get enumerated in different orders, perhaps based on the numerical value of a pointer or something arbitrary like that.)

You can use GCD or -performSelector:withObject:afterDelay: to remove your observation after the current iteration of the runloop completes. This doesn't guarantee that you wont receive any more notifications though, so if you need that guarantee, you have to build a check into the listener's state for this condition.

Lastly, please use contexts for all your KVO observances. For details see the explanation I posted over here.

Community
  • 1
  • 1
ipmcc
  • 29,581
  • 5
  • 84
  • 147