0
votes

This is by far the weirdest problem I've been stuck with.

I have a UIViewController on a UINavigationController and I want to call a method at viewDidAppear using NSInvocationOperation so it can run on a back thread when the view becomes visible.

The problem is that if I pop the view controller BEFORE the operation (in this case the testMethod method) completes running, the app crashes.

Everything works fine if I pop the view controller AFTER the operation runs it's course.

When the app crashes, it stops at [super dealloc] with "EXC-BAD-ACCESS" and gives me the following error.

bool _WebTryThreadLock(bool), xxxxxxxxx: 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...

And this is my code (super simplified)..

- (void)viewDidAppear:(BOOL)animated
{
     [super viewDidAppear:animated];

    NSInvocationOperation *theOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testMethod) object:nil];
    [operationQueue addOperation:theOperation];
    [theOperation release];
}

- (void)testMethod
{
    NSLog(@"do some stuff that takes a few seconds to complete");
}

- (void)dealloc
{
    [_tableView release];
    [super dealloc];
}

The testMethod has some code that takes a few seconds to complete. I only have a few clues and I really don't know how and where to start debugging this.

  • Clue #1: The funniest thing is that if I remove the [_tableView release]; from dealloc then the app doesn't crash. But of course this would cause a leak and I can't remove it.

  • Clue #2: I've tested this code on a separate "clean" UIViewController with a UITableView and to my surprise it didn't crash.

  • Clue #3: The app doesn't crash is the UITableView's datasource is set to nil in viewDidLoad.

  • Clue #4: The app doesn't seem crash if I use the same code in viewDidAppear somewhere else like an IBAction.

  • Clue #5: I've tried looking over stack data with NSZombie but it gives me tons of data and it leads me nowhere.

I have some very complicated code within my UITableViewDelegate and UITableViewDataSource and I really don't know where to start debugging this. I really hope I don't have to go through line by line or rewrite the entire thing because of this.

Any pointers on where I should be looking?

2
Are you accessing your view controller anywhere within testMethod (e.g. by calling any method on self)? That would explain the crash because when you pop the view controller, it (probably) gets released but the operation still references the dangling pointer.omz
I've tried emptying testMethod and delayed it from running for 5 seconds to test if it was anything in the method that was causing it but even a simple NSLog will crash the app consistently.Jiho Kang
I'm not entirely sure about this because the documentation on NSInvocation is a bit unclear, but it seems to me that the invocation doesn't retain its target (your view controller) which would be a problem here because it tries to call testMethod after it's released.omz
You could try to retain self when you add the operation and balance it with a release when it's finished. – Or use an NSBlockOperation.omz
adding [self retain] to testMethod and adding [self release] to dealloc also seems to work, only I feel nervous about leaks and over releasing. Jason's fix seems to work very well. Thanks for the input!Jiho Kang

2 Answers

2
votes

The problem is likely that your view controller's last reference is the operation queue holding onto it, which means you are technically calling (or having the system call) some UIKit methods in a background thread (a big no-no) when the operation cleans up.

To prevent this from happening, you need to send a keep-alive message to your controller on the main thread at the end of your operation, by adding something like this to the last line in your testMethod:

[self performSelectorOnMainThread:@selector(description) withObject:nil waitUntilDone:NO];

There is still a chance that this message may get processed before the operation queue releases your view controller, but that chance is pretty remote. If it's still happening, you could do something like this:

[self performSelectorOnMainThread:@selector(keepAlive:) 
                       withObject:[NSNumber numberWithBool:YES]
                    waitUntilDone:NO];

- (void)keepAlive:(NSNumber *)fromBackground
{
    if (fromBackground)
         [self performSelector:@selector(keepAlive:) withObject:nil afterDelay:1];
}

By sending a message to your view controller on the main thread, it will keep the object alive (NSObject retains your view controller until the main thread handles the message). It will also keep the view controller alive if you perform a selector after a delay.

1
votes

You're crashing because the controller is still trying to use your tableView reference and since you poped the viewController, everything will go away in the dealloc and the tableView is still populating itself. You can try asking in your dealloc method if your operation is still running, so you can cancel it and the everything should be fine.

Once you add an operation to a queue, the operation is out of your hands. The queue takes over and handles the scheduling of that task. However, if you decide later that you do not want to execute the operation after all—because the user pressed a cancel button in a progress panel or quit the application, for example—you can cancel the operation to prevent it from consuming CPU time needlessly. You do this by calling the cancel method of the operation object itself or by calling the cancelAllOperations method of the NSOperationQueue class.

Cancelling an operation does not immediately force it to stop what it is doing. Although respecting the value returned by the isCancelled is expected of all operations, your code must explicitly check the value returned by this method and abort as needed. The default implementation of NSOperation does include checks for cancellation. For example, if you cancel an operation before its start method is called, the start method exits without starting the task.