Search code examples
objective-ciosmultithreadingkey-value-observingnsoperationqueue

Crashing in observeValueForKeyPath which attached to NSOperationQueue operationsCount


I have UITextView logView (tried both atomic and nonatomic) and NSOperationQueue uploadQueue properties in my UIView subclass. I've added KVO on my NSOperationQueue property's operationsCount like this:

[[self uploadQueue] addObserver:self
            forKeyPath:@"operationCount" 
               options:(NSKeyValueObservingOptionNew |
                        NSKeyValueObservingOptionOld)
               context:NULL];

Observer function looks like this:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([object isKindOfClass: [NSOperationQueue class]]) {
        NSLog(@"Keypath is: %@ change dictionary is: %@", keyPath, change);
        NSInteger testnew = [[change objectForKey: @"new"] integerValue];
        NSInteger testold = [[change objectForKey: @"old"] integerValue];
        if (testnew > testold) {
            [[self logView] setText: [NSString stringWithFormat: @"Uploading %d files", testnew]];
            objc_setAssociatedObject([self logView], @"max_value_of_uploads", [change objectForKey: @"new"], OBJC_ASSOCIATION_COPY);
        } else {
            NSInteger value = [objc_getAssociatedObject([self logView], @"max_value_of_uploads") integerValue]; 
            [[self logView] setText: [NSString stringWithFormat: @"Uploaded %d of %d files", testnew, value]];
        }
    }
}

uploadQueue filling works like that:

   ...
   NSDictionary *iter;
        NSEnumerator *enumerator = [objects objectEnumerator];
        while (iter = [enumerator nextObject]) {
            Uploader *op = [[Uploader alloc] initWithFileName: [iter objectForKey: @"fileName"] 
                                                             FileID: [[iter objectForKey: @"fileID"] integerValue] 
                                                       AndSessionID: [self sess]];
            //Uploader *op = [[Uploader alloc] init];
            [[self uploadQueue] addOperation: op];
   ...

Without if-else block, everything works just fine: I've got NSLog messages about number of operations in queue, numbers are ok and so on. But with that if-else block, I get crash, which looks like this:

bool _WebTryThreadLock(bool), 0x10617fb0: Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now...
1   WebThreadLock
2   -[UITextView setText:]
3   -[SincViewController observeValueForKeyPath:ofObject:change:context:]
4   NSKeyValueNotifyObserver
5   NSKeyValueDidChange
6   -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
7   -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:usingBlock:]
8   ____NSOQDelayedFinishOperations_block_invoke_0
9   -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
10  -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:usingBlock:]
11  __NSOQDelayedFinishOperations
12  _dispatch_after_timer_callback
13  _dispatch_source_invoke
14  _dispatch_queue_invoke
15  _dispatch_worker_thread2
16  _pthread_wqthread
17  start_wqthread

It crashes in else block. Moreover, I don't see any changes to my logView. Thanks for future help and responses.

UPDATE: performSelectorOnMainThread when setting text saved me.


Solution

  • When updating your UI, make sure that you're in the main thread. A possible solution follows:

    dispatch_async(dispatch_get_main_queue(), ^{
        if (testnew > testold) {
            [[self logView] setText: [NSString stringWithFormat: @"Uploading %d files", testnew]];
            objc_setAssociatedObject([self logView], @"max_value_of_uploads", [change objectForKey: @"new"], OBJC_ASSOCIATION_COPY);
        } else {
            NSInteger value = [objc_getAssociatedObject([self logView], @"max_value_of_uploads") integerValue]; 
            [[self logView] setText: [NSString stringWithFormat: @"Uploaded %d of %d files", testnew, value]];
        }
    }); 
    

    Note that using dispatch_async or dispatch_sync depends on your desired implementation, but in the latter case, you risk deadlocking unless you check what thread you're in.

    See also: NSOperation, observer and thread error where someone suggests moving your UI code to a separate method:

    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
      [self performSelectorOnMainThread:@selector(dataLoadingFinished:) withObject:nil waitUntilDone:YES];
    }
    

    .. which is a fine alternate route.