Why my completionBlock never gets called in an NSO

2019-03-25 08:46发布

问题:

I've sublcassed an NSOperation and set my completionBlock but it seems to never enter even when the operation finishes. Here's my code:

A catalog controller class sets up the NSOperation:

- (void)setupOperation {
...

    ImportWordOperation *importWordOperation = [[ImportWordOperation alloc] initWithCatalog:words];
    [importWordOperation setMainObjectContext:[app managedObjectContext]];
    [importWordOperation setCompletionBlock:^{
        [(ViewController *)[[app window] rootViewController] fetchResults];
    }];
    [[NSOperationQueue mainQueue] addOperation:importWordOperation];
    [importWordOperation release];
...
}

As you can see, I'm setting the completion block to execute a method on the main thread, in some other controller.

Then, in main my subclassed NSOperation class: ImportWordOperation.m, I fire-up the background operation. I even overrode isFinished iVar in order for the completion method to be triggered:

- (void)setFinished:(BOOL)_finished {
    finished = _finished;
}

- (BOOL)isFinished {
    return (self.isCancelled ? YES: finished);
}

- (void)addWords:(NSDictionary *)userInfo {
    NSError *error = nil;

    AppDelegate *app = [AppDelegate sharedInstance];

    NSManagedObjectContext *localMOC = [userInfo valueForKey:@"localMOC"];
    NSEntityDescription *ent = [NSEntityDescription entityForName:@"Word" inManagedObjectContext:localMOC];
    for (NSDictionary *dictWord in [userInfo objectForKey:@"words"]) {
        Word *wordN = [[Word alloc] initWithEntity:ent insertIntoManagedObjectContext:localMOC];

        [wordN setValuesForKeysWithDictionary:dictWord];
        [wordN release];
    }

    if (![[userInfo valueForKey:@"localMOC"] save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    [localMOC reset];

    [self setFinished:YES];
}


- (void)main {

    finished = NO;

    NSManagedObjectContext *localMOC = nil;
    NSUInteger type = NSConfinementConcurrencyType;
    localMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:type];
    [localMOC setUndoManager:nil];
    [localMOC setParentContext:[self mainObjectContext]];

    if (![self isCancelled]) {
        if ([self.words count] > 0) {
            [self performSelectorInBackground:@selector(addWords:) withObject:@{@"words":self.words, @"localMOC":localMOC}];
        }
    }
}

If I remove isFinished accessor methods, then the completion block gets called but way before ImportWordOperation finishes.

I've read code that I've found that uses its own completion block but then what's the use for the completion block in NSOperation subclasses anyway?

Any ideas or point to a similar solved situation would be greatly appreciated.

回答1:

You've kind of fallen into a weird space between concurrent and non-concurrent NSOperation subclasses here. Typically, when you implement main, your operation is non-concurrent, and isFinished changes to YES as soon as main exits.

However, you've provided your own implementation of isFinished, and also coded it so that isFinished doesn't return YES until after main has exited. This makes your operation start to act like a concurrent operation in many ways - at least including the need to manually emit KVO notifications.

The quick solution to your problem is to implement setFinished: using (will|did)ChangeValueForKey: calls. (I also changed the ivar name to reflect naming prevailing naming conventions). Below is an NSOperation subclass that I believe accurately models your operation's workings, in terms of finishing in a concurrent fashion.

@implementation TestOperation {
    BOOL _isFinished;
}

- (void)setFinished:(BOOL)isFinished
{
    [self willChangeValueForKey:@"isFinished"];
    // Instance variable has the underscore prefix rather than the local
    _isFinished = isFinished;
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isFinished
{
    return ([self isCancelled] ? YES : _isFinished);
}

- (void)main
{
    NSLog(@"%@ is in main.",self);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self setFinished:YES];
    });
}

@end

I'm not familiar with your requirements so perhaps you have a pressing need, but your operation would seem a more natural fit for a concurrent operation that uses start instead of main. I've implemented a small example that appears to be working correctly.

@implementation TestOperation {
    BOOL _isFinished;
    BOOL _isExecuting;
}

- (void)setFinished:(BOOL)isFinished
{
    if (isFinished != _isFinished) {
        [self willChangeValueForKey:@"isFinished"];
        // Instance variable has the underscore prefix rather than the local
        _isFinished = isFinished;
        [self didChangeValueForKey:@"isFinished"];
    }
}

- (BOOL)isFinished
{
    return _isFinished || [self isCancelled];
}

- (void)cancel
{
    [super cancel];
    if ([self isExecuting]) {
        [self setExecuting:NO];
        [self setFinished:YES];
    }
}

- (void)setExecuting:(BOOL)isExecuting {
    if (isExecuting != _isExecuting) {
        [self willChangeValueForKey:@"isExecuting"];
        _isExecuting = isExecuting;
        [self didChangeValueForKey:@"isExecuting"];
    }
}

- (BOOL)isExecuting
{
    return _isExecuting;
}

- (void)start
{
    NSLog(@"%@ is in start. isCancelled = %@", self, [self isCancelled] ? @"YES" : @"NO");
    if (![self isCancelled]) {
        [self setFinished:NO];
        [self setExecuting:YES];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
            sleep(1);
            [self setExecuting:NO];
            [self setFinished:YES];
        });
    }
}
@end


回答2:

I encountered this error while implementing an asynchronous subclass of NSOperation.

The Swift way of referencing key paths is with the #keyPath directive, so I was doing this (_executing and _finished are my internal variables):

self.willChangeValue(forKey: #keyPath(Operation.isExecuting))
self._executing = false
self.didChangeValue(forKey: #keyPath(Operation.isExecuting))

self.willChangeValue(forKey: #keyPath(Operation.isFinished))
self._finished = true
self.didChangeValue(forKey: #keyPath(Operation.isFinished))

Unfortunately, the #keyPath expressions above resolve to "executing" and "finished", respectively, and we need to throw KVO notifications for "isExecuting" and "isFinished". This is why the completionBlock is not getting invoked.

The solution is to hard code them as such:

self.willChangeValue(forKey: "isExecuting")
self._executing = false
self.didChangeValue(forKey: "isExecuting")

self.willChangeValue(forKey: "isFinished")
self._finished = true
self.didChangeValue(forKey: "isFinished")