NSOperationQueue's addOperation: operation is finished and cannot be enqueued?

Frederick C. Lee picture Frederick C. Lee · May 30, 2013 · Viewed 8.1k times · Source

Note:
This is merely a proof-of-concept.
The real background task will be to continuously request 'HTTP Get' of raw data and to display it via main thread; on demand.

Scenario:
1) Toggle a background task (loop) on demand.
2) Background task notifies main thread UI per iteration.
3) Only one (1) block operation runs in the queue.

Modus Operandus
1) Use NSBlockOperation to contain the background code.
2) Use a regional BOOL to toggle the loop; via IBAction.

Problems
1) The compiler is flagging the BOOL 'isRunning' as a strong link:

Capturing 'self' strongly in this block is likely to lead to a retain cycle.


2) I checked to see if any operations are on the queue before attempting to add the block operation.
But I always get the following error:

-[NSOperationQueue addOperation:]: operation is finished and cannot be enqueued

This proof-of-concept appears to be working, besides the stated problems.

Question:
1) Why is the compile flagging the BOOL 'running' as a strong line when it's merely a scaler?
2) Why can't I reuse the NSOperationQueue via adding another NSBlockOperation if none is found in the queue?

The following is the entire code:

#define START 0
#define STOP 1

@interface ricViewController ()
@property (assign) BOOL running;
@end

@implementation ricViewController {
    NSOperationQueue *operationQueue;
    NSBlockOperation *blockOperation;
    void (^backgroundBlock)(void);
}

@synthesize running = isRunning;

#pragma mark - ViewController methods

- (void)viewDidLoad {
    operationQueue = [NSOperationQueue new];
    [operationQueue setMaxConcurrentOperationCount:1];
    [operationQueue setName:@"RicQueue"];
    [self buildBackgroundBlock];
    blockOperation = [NSBlockOperation blockOperationWithBlock:backgroundBlock];
    [super viewDidLoad];
}

// -------------------------------------------------------------------------------------------------------------------

- (void)didReceiveMemoryWarning {
    operationQueue = nil;
}

// -------------------------------------------------------------------------------------------------------------------
#pragma mark - Local methods

- (void)buildBackgroundBlock {
    static int k = 0;
    backgroundBlock = ^{
        while (isRunning) {   // 1) *** compiler warning flag: strong link warning ***
            sleep(1);
            if ([NSThread isMainThread]) {
                NSLog(@"{backgroundBlock} *** Main Thread *** ***");
            } else {
                 NSString *myString = [NSString stringWithFormat:@"{backgroundBlock} count = %i", k++];
                 NSLog(myString);
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.dataLabel.text = myString;
                });

            }
        }
    };
}

// -------------------------------------------------------------------------------------------------------------------
#pragma - Action methods

- (IBAction)exitAction:(UIButton *)sender {
    exit(0);
}

// -------------------------------------------------------------------------------------------------------------------

- (IBAction)segmentedAction:(UISegmentedControl *)sender {

    switch (sender.selectedSegmentIndex) {
        case START:
            NSLog(@"START");
            self.running = YES;
            if (operationQueue.operationCount < 1) {
                [operationQueue addOperation:blockOperation]; // 2) *** fatal error on 2nd pass.
            }
            break;
        case STOP:
            NSLog(@"STOP");
            self.running = NO;
            break;
    }
    return;
}

@end

The console output:

BackgroundTask[3759:c07] STOP
BackgroundTask[3759:c07] START
BackgroundTask[3759:1303] {backgroundBlock} count = 0
BackgroundTask[3759:1303] {backgroundBlock} count = 1
BackgroundTask[3759:1303] {backgroundBlock} count = 2
BackgroundTask[3759:1303] {backgroundBlock} count = 3
BackgroundTask[3759:1303] {backgroundBlock} count = 4
BackgroundTask[3759:1303] {backgroundBlock} count = 5
BackgroundTask[3759:1303] {backgroundBlock} count = 6
BackgroundTask[3759:1303] {backgroundBlock} count = 7
BackgroundTask[3759:c07] STOP
BackgroundTask[3759:1303] {backgroundBlock} count = 8

Answer

Frederick C. Lee picture Frederick C. Lee · May 30, 2013

I did further research and found that
* I must re-create the NSBlockOperation object per each 'addOperation' *, since NSOperationQueue refuses to re-queue the SAME NSOperation object.

Hence the following solution:

- (IBAction)segmentedAction:(UISegmentedControl *)sender {
    switch (sender.selectedSegmentIndex) {
        case START:
            NSLog(@"START");
            self.running = YES;
            blockOperation = nil;
            [self buildBackgroundBlock];
            blockOperation = [NSBlockOperation blockOperationWithBlock:backgroundBlock];
            [operationQueue addOperation:blockOperation];
            break;
        case STOP:
            NSLog(@"STOP");
            self.running = NO;
            break;
    }
}

...as for the compiler associates the BOOL 'isRunning' with the static 'self':

Capturing 'self' strongly in this block is likely to lead to a retrain cycle.

All references to 'self' should be via a weak link.

- (void)buildBackgroundBlock {
    static int k = 0;
    BOOL myRunning = isRunning;
    __weak ricViewController *weakObject = self;

    backgroundBlock = ^{
        while (myRunning) {
            sleep(1);
            if ([NSThread isMainThread]) {
                NSLog(@"{backgroundBlock} *** Main Thread *** ***");
            } else {
                NSString *myString = [NSString stringWithFormat:@"{backgroundBlock} count = %i", k++];
                NSLog(myString);
                dispatch_async(dispatch_get_main_queue(), ^{
                weakObject.dataLabel.text = [myString copy];
                });
            }
        }
    };
}