9
votes

I have a UIWebView in a viewcontroller, which has two methods as below. The question is if I pop out(tap back on navigation bar) this controller before the second thread is done, the app will crash after [super dealloc], because "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.". Any help would be really appreciated.

-(void)viewDidAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(load) object:nil];
    [operationQueue addOperation:operation];
    [operation release];
}

-(void)load {
    [NSThread sleepForTimeInterval:5];
    [self performSelectorOnMainThread:@selector(done) withObject:nil waitUntilDone:NO];
}
8

8 Answers

35
votes

I had the same solution where a background thread was the last release, causing dealloc of view controller to happen in a background thread ending up with the same crash.

The above [[self retain] autorelease] would still result in the final release happening from the autorelease pool of the background thread. (Unless there's something special about releases from the autorelease pool, I'm surprised this would make a difference).

I found this as my ideal solution, placing this code into my view controller class:

- (oneway void)release
{
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(release) withObject:nil waitUntilDone:NO];
    } else {
        [super release];
    }
}

This ensures that the release method of my view controller class is always executed on the main thread.

I'm a little surprised that certain objects which can only be correctly dealloc'ed from the main thread don't already have something like this built in. Oh well...

12
votes

Here is some code to run UIKit elements on the main thread. If you're working on a different thread and need to run a piece of UIKit code, just put it inbetween the brackets of this Grand Central Dispatch snippet.

dispatch_async(dispatch_get_main_queue(), ^{

    // do work here

});
1
votes

In general, you should cancel any background operations when the view that uses them is going away. As in:

- (void)viewWillDisappear:(BOOL)animated {
  [operationQueue cancelAllOperations];
  [super viewWillDisappear:animated;
}
1
votes

I currently have a similar problem in my application. A view controller which displays a UIWebView is pushed to a navigation controller and starts a background thread to retrieve data. If you hit the back button before the thread has finished, the application crashes with the same error message.

The problem seems to be that NSThread retains the target (self) and object (argument) and releases it after the method has been run -- unfortunately it releases both from within the thread. So when the controller gets created, the retain count is 1, when the thread is started, the controller gets a retain count of 2. When you pop the controller before the thread is done, the navigation controller releases the controller, which results in a retain count of 1. So far this is fine -- But if the thread finally finishes, NSThread releases the controller, which results in a retain count of 0 and an immediate dealloc from within the thread. This makes the UIWebView (which is released in the dealloc method of the controller) raise that thread warning exception and crash.

I successfully worked around this by using [[self retain] autorelease] as the last statement in the thread (right before the thread releases its pool). This ensures that the controller object is not released immediately but marked as autoreleased and deallocated later in the main thread's run loop. However this is a somewhat dirty hack and I'd rather prefer to find a better solution.

1
votes

I tried :

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

which seems to work even better.

0
votes

I'm not sure exactly what's going based on your code, but it looks like viewDidAppear is getting called and creating the second thread, and then you're navigating away from the controller and releasing it, and then the second thread finishes and calls performSelectorOnMainThread on the released object "self". I think you may just need to check that the release has not occurred?

The error message you're getting implies that you're running some UIKit code from your second thread. Apple recently added some checks for threaded calls to UIKit, and I think you probably just need to refactor your load function to update the UI on the main thread, instead of calling UIWebView functions from the second thread.

Hope that helps!

0
votes

I tried both solutions posted above, [operationQueue cancelAllOperations], and [[self retain] autorelease]. However, with quick clicking, there are still cases where the retain count falls to 0 and the class gets deallocated on the secondary thread. In order to avoid a crash, I put the following in my dealloc for now:

    if ([NSThread isMainThread]) {
        [super dealloc];
    }

which is an obvious leak, but seems to be the lesser of 2 evils.

Any additional insight from anyone encountering this problem is welcome.

0
votes
- (void)dealloc
{
    if(![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(dealloc) 
                               withObject:nil 
                            waitUntilDone:[NSThread isMainThread]];
        return;
    } 
    [super dealloc];
}