2
votes


I'm trying to build a download manager class that packets all the async download (every op has its own thread) operation in an NSOperation subclass to add them later in an NSOperationQueue. The download manager class (a singleton) also exposes few methods to work on the queue and cancel operations that matches some requirements.
Those are steps to start creating kind of a Class Cluster(Abstract Factory) that returns different kinds of NSOperation for different types of common operation (upload, download,parse, etc).
The class seems to work pretty well with download operations, but if in the middle of those operations I call a method for cancel an operation, the operation is successfully cancelled but the app crashes few operation later. If I don't cancel any operations everything works fine. All operations are observed using KVO. The method that remove the operation looks like that:

- (void) cancelDownloadOperationWithID:(NSString *)aUUID{
@synchronized(self){
    [self.dowloadQueue setSuspended:YES]; //downloadQueue is an NSOperationQueue
    NSArray * downloadOperations = [self.dowloadQueue operations];
    NSPredicate * aPredicate = [NSPredicate predicateWithFormat:@"SELF.connectionID == %@",aUUID]; //SELF is the signleton instance of the download manager
    NSArray * filteredArray = [downloadOperations filteredArrayUsingPredicate:aPredicate];
    if ([filteredArray count]==0) {
        [self.dowloadQueue setSuspended:NO];
        return;
    } 
    [filteredArray makeObjectsPerformSelector:@selector(cancel)];
    NSLog(@"Cancelled %d operations",[filteredArray count]);
    [self.dowloadQueue setSuspended:NO];
   }
}

The crash log is pretty incomprehensible but is a BAD_EXC_ACCESS (a zombie perhaps), notice that I'm under ARC.

0x00a90ea8  <+0393>  jle    0xa90d9f <____NSOQSchedule_block_invoke_0+128>
0x00a90eae  <+0399>  mov    -0x38(%ebp),%ecx
0x00a90eb1  <+0402>  mov    -0x34(%ebp),%esi
0x00a90eb4  <+0405>  mov    (%esi,%ecx,1),%ecx
0x00a90eb7  <+0408>  mov    -0x40(%ebp),%esi
0x00a90eba  <+0411>  cmpb   $0x0,(%ecx,%esi,1)
0x00a90ebe  <+0415>  jne    0xa90d9f <____NSOQSchedule_block_invoke_0+128>
0x00a90ec4  <+0421>  mov    (%edi,%eax,1),%esi
0x00a90ec7  <+0424>  mov    (%esi,%edx,1),%ebx
0x00a90eca  <+0427>  mov    %ebx,-0x2c(%ebp)
0x00a90ecd  <+0430>  mov    -0x44(%ebp),%ebx
0x00a90ed0  <+0433>  cmpl   $0x50,(%esi,%ebx,1)
0x00a90ed4  <+0437>  mov    %edi,%ebx
0x00a90ed6  <+0439>  jne    0xa90e96 <____NSOQSchedule_block_invoke_0+375>
0x00a90ed8  <+0441>  mov    -0x48(%ebp),%ebx
0x00a90edb  <+0444>  cmpb   $0x0,(%esi,%ebx,1)
0x00a90edf  <+0448>  mov    %edi,%ebx
0x00a90ee1  <+0450>  je     0xa90e96 <____NSOQSchedule_block_invoke_0+375>

Can some one give me suggestion about it?
Thanx Andrea

3

3 Answers

4
votes

Well the answer was pretty simple. In the overridden -cancel method of the NSOperation subclass I was setting both the finished and executing vars triggering the proper KVO callbacks. The problem is that an operation stays in the NSOperationQueue even if it is cancelled, when the queue tries to launch the -start method on an NSOperationQueue that has triggered its KVO callback it crashes.

The work around is as follows: If the operation was cancelled while it was not executing, you must set the finish var to YES right after the start method implementation, otherwise if it was executing it's ok to set finished to YES and executing to NO.

3
votes

The accepted answer works for me. Just to help clear this up in case anybody else runs into it, I also experienced this crash by setting isFinished improperly inside of my - cancel, before an async operation had begun executing.

Rather than doing that, I switched my - cancel to only change isFinished if the operation was already isExecuting, then in - start I set isFinished immediately as suggested here. VoilĂ , crash gone.

1
votes

Here's a piece in swift using two previous answers:

override func cancel() {
    super.cancel()

    if executing {
        executing = false
        finished = true
    }

    task.cancel()
}

override func start() {
    if cancelled {
        finished = true
        return
    }

    executing = true

    main()
}

override func main() {
    task.resume()
}