开发者

Updating a deallocated UIWebView from a background thread

(Two edits follow the original body of this question, both of which modify the question pretty radically. Don't get hung up on the first part of this--while useful for contextual purposes, I've all but ruled out the original problem I was asking about.)

As you can see from the title, I've programmed myself into a corner and I've got several thi开发者_开发问答ngs working against me...

In a UIViewController subclass that manages a large and complex view. One part of it is a UIWebView that contains output from a web request that I had to build and execute, and manually assemble HTML from. Since it takes a second or two to run, I dropped it into the background by calling self performSelectorInBackground:. Then from that method I call there, I use self performSelectorOnMainThread: to get back to the surface of the thread stack to update the UIWebView with what I just got.

Like this (which I've cut down to show only the relevant issues):

-(void)locationManager:(CLLocationManager *)manager
   didUpdateToLocation:(CLLocation *)newLocation
          fromLocation:(CLLocation *)oldLocation
{
    //then get mapquest directions
    NSLog(@"Got called to handle new location!");

    [manager stopUpdatingLocation];

    [self performSelectorInBackground:@selector(getDirectionsFromHere:) withObject:newLocation];

}

- (void)getDirectionsFromHere:(CLLocation *)newLocation
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    CLLocationCoordinate2D here = newLocation.coordinate;

 // assemble a call to the MapQuest directions API in NSString *dirURL
 // ...cut for brevity

    NSLog(@"Query is %@", dirURL);
    NSString *response = [NSString stringWithContentsOfURL:[NSURL URLWithString:dirURL] encoding:NSUTF8StringEncoding error:NULL];
    NSMutableString *directionsOutput = [[NSMutableString alloc] init];

// assemble response into an HTML table in NSString *directionsOutput
// ...cut for brevity

    [self performSelectorOnMainThread:@selector(updateDirectionsWithHtml:) withObject:directionsOutput waitUntilDone:NO];
    [directionsOutput release];
    [pool drain];
    [pool release];
}


- (void)updateDirectionsWithHtml:(NSString *)directionsOutput
{
    [self.directionsWebView loadHTMLString:directionsOutput baseURL:nil];
}

This all works totally great, UNLESS I've backed out of this view controller before CLLocationManager hits its delegate method. If this happens after I've already left this view, I get:

2010-06-07 16:38:08.508 EverWondr[180:760b] bool _WebTryThreadLock(bool), 0x1b6830: 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...

Despite what this says, I can repeatably cause this crash when I back out too early. I'm not at all convinced that attempting a UI update from a background thread is really the issue; I think it's that my UIWebView is deallocated. I suspect that the fact I was just IN a background thread makes the runtime suspect something's up about that, but I feel fairly sure that's not it.

So how do I tell CLLocationManager not to worry about it, when I'm backing out of that view? I tried [self.locationManager stopUpdatingLocation] inside my viewWillDisappear method, but that didn't do it.

(Incidentally, MapQuest's apis are FANTASTIC. Way WAY better than anything Google provides. I can't recommend them highly enough.)

EDIT 1:

This just got weirder. I've actually isolated where it crashes, and it's at the bottom of -(void) dealloc. If I comment out [super dealloc], I don't crash! But I can't seem to "step into" [super dealloc] to see what's happening in the superclass, it just crashes and stops talking to me.

Here's a stacktrace:

#0  0x3018c380 in _WebTryThreadLock ()
#1  0x3018cac8 in _WebThreadAutoLock ()
#2  0x3256e4f0 in -[UITextView dealloc] ()
#3  0x323d7640 in -[NSObject release] ()
#4  0x324adb34 in -[UIView(Hierarchy) removeFromSuperview] ()
#5  0x3256e4a8 in -[UIScrollView removeFromSuperview] ()
#6  0x3256e3e4 in -[UITextView removeFromSuperview] ()
#7  0x324ffa24 in -[UIView dealloc] ()
#8  0x323d7640 in -[NSObject release] ()
#9  0x3256dd64 in -[UIViewController dealloc] ()
#10 0x0000c236 in -[EventsDetailViewController dealloc] (self=0x1c8360, _cmd=0x3321ff2c) at /Users/danielray/Documents/Xcode Projects/EverWondr svn/source/obj-c/Classes/EventsDetailViewController.m:616
#11 0x323d7640 in -[NSObject release] ()
#12 0x336e0352 in __NSFinalizeThreadData ()
#13 0x33ad8e44 in _pthread_tsd_cleanup ()
#14 0x33ad8948 in _pthread_exit ()
#15 0x33731f02 in +[NSThread exit] ()
#16 0x336dfd2a in __NSThread__main__ ()
#17 0x33ad8788 in _pthread_body ()
#18 0x00000000 in ?? ()

Also, it's now clear that the error happens when the back button is fired while [self getDirectionsFromHere] is running. The window for that is while we're waiting for the stringWithContentsOfURL call to return.

EDIT 2:

Okay, so noticing the [UITextView dealloc] near the top of that stack, I realized I only have one UITextView on this view hierarchy, so I commented out the release of that UITextView object in -(void) dealloc. Next crash, there was [UIWebView dealloc] in that spot of the trace. Hm! A change! So I commented out my one webView's release in the dealloc method... and now I don't crash. This isn't a solution, because as far as I can tell I'm just leaking those two objects now, but it sure is interesting. Or at least, I hope it is to somebody, because I'm totally at a loss.


I think your diagnosis is probably wrong, but let's imagine for the moment that it isn't and see what you might do about it.

If the problem was the web view being accessed after deallocation, in response to the sequence of calls shown above, then that call would be coming from updateDirectionsWithHtml: through the reference in self.directionsWebView. In which case, you should just ensure that you don't keep the stale pointer around, but set it to nil. However, I think it's a fair bet you do do that already, so it's unlikely this is the problem.

If the problem was the controller being accessed after deallocation, by the location manager invoking the delegate method, then you should make sure to break the link first. Something like [locationManager setDelegate:nil] ought to stop the unwanted call even if the location manager itself outlives your controller. But again, I suspect you're already releasing the location manager before the controller dies, and this isn't the problem either.

In which case, the problem is most likely elsewhere, and it might be worth considering the error message at face value: is it possible that somewhere you are calling UIKit from a background thread? Saying that the runtime is confused about this because you were in a background thread earlier seems just a teensy bit questionable to me. That error looks like it's coming from somewhere fairly concrete. That place could be buggy, but it's usually better to start off with the assumption that the OS works better than one's own code.

Without seeing the rest of the code it's difficult to know where this real problem, if it truly exist, may be. One possibility that springs to mind is that you could have some innocent-looking use of a property that actually turns out to hit the UI somewhere; but I admit that's just idle speculation.

Update:

If putting the background call into the foreground prevents you from hitting the back button, then the time consuming operation must already be in progress, which means your delegate method must already have been called -- the logging telling you so just isn't getting through.

One thing that occurs to me here is that both performSelectorInBackground and performSelectorOnMainThread retain their receiver until after the selector has finished running. So, the timing of your controller's deallocation is probably screwed up as a result (unless you're forcing it by an over-release somewhere).

From your stack trace, it almost looks as if dealloc is actually being called from within the background thread as part of its own cleanup. This then calls through to various UIKit things, which would explain the thread lock error. But if performSelectorOnMainThread is being called that should extend the controller lifetime until it's back on the main thread. So maybe there is an over-release after all?

Update 2:

This discussion is getting a bit scattered about, but let me try to summarise what I think is happening:

  • When you call [self performSelectorInBackground:...], a new thread is created and self gets sent a retain message by the thread.
  • Sometime later, you hit the back button and cause your controller to get sent a release. However, because it was retained by the background thread, it doesn't get deallocated yet.
  • One way or another the background thread finishes, at which point it sends a release message to the controller. The controller's retain count drops to 0 and its dealloc method gets called. But since this message is coming from a background thread, when it tries to do UI stuff (removing subviews) you get the thread lock error.

In justification of this last bit, which I understand might seem a bit strange, note that the call stack from the crash shows the calls coming from a thread finishing, rather than from any event handling associated with your back button.

Now, the interesting question here is not why the background thread is sending release, which is a natural part of its lifecycle, but why this is reducing the retain count to 0. If the thread is actually getting as far as performSelectorOnMainThread, there should be yet another retain message, and your controller should survive long enough to dealloc on the main thread, which ought to be fine.

It seems to me there are two possible causes: either the thread is exiting prematurely, or it is being over-released. But if it were the latter, you'd expect that self would already hit dealloc before the thread terminates.

So I guess the question is, is there some way that updateDirectionsWithHtml would exit without calling performSelectorOnMainThread? Or otherwise, is there something else that would cause the thread to terminate prematurely?


You have set yourself as the delegate on the CLLocationManager. Delegates are almost always implemented as weak references, meaning they are not retained. This means when your view controller is popped off the navigation stack and released the CLLocationManager now has a bad pointer and you will get crashes and all sorts of horrible hard-to-debug problems.

Standard practice is to set the CLLocationManager's delegate to nil in your dealloc method, thus making sure it doesn't try to call you after you have gone. i.e:

- (void)dealloc {
    locationManager.delegate = nil;
    [locationManager stopUpdatingLocation];
    [locationManager release];
    // ...
    [super dealloc];
}

As far as I can tell your performSelectorInBackground etc. code is OK, all those calls retain the receiver until the selector returns so I don't think that is the source of your problems.


OK, I'll post this as another answer since my previous one is still valid. You are over-releasing the auto release pool.

[pool drain];
[pool release];

drain is identical to release. From the documentation:

In a reference-counted environment, this method behaves the same as release. Since an autorelease pool cannot be retained (see retain), this therefore causes the receiver to be deallocated.

You should only call release or drain, not both. Over-releasing the pool is going to cause a double-free and again random hard-to-debug problems.

You might want to try NSZombieEnabled to further diagnose this.


The problem is that the final release (and therefore) the dealloc is getting called on a thread other than the main thread. There are a few ways to handle the situation. One is to do your background processing on a different object that has a weak reference to the view controller. It might look something like:

@interface MyAppViewController : UIViewController {
  MyAppLocationUpdater *locationUpdater;
}
@property MyAppLocationUpdater* locationUpdater;
@end

@implementation MyAppViewController
@synthesize locationUpdater;
-(void)alloc
{
  [super alloc];
  locationUpdater = [[MyAppLocationUpdator alloc] init];
  locationUpdater.viewController = self;
}

-(void)dealloc
{
  locationUpdater.viewController = nil;
  [locationUpdater release];
}

-(void)locationManager:(CLLocationManager*)manager
   didUpdateToLocation:(CLLocation *)newLocation
          fromLocation:(CLLocation *)oldLocation
{
  [manager stopUpdatingLocation];
  [locationUpdater performSelectorInBackgroundThread: @selector(getDirectionsFromHere:) withObject:newLocation];
}
@end

@interface MyAppLocationUpdater : NSObject {
  // this reference is not retained or released, the view controller is responsible for clearing the field when it is deallocating
  volatile MyAppViewControler *viewController;
}
-(void)getDirectionsFromHere:(CLLocation *)newLocation;
@end

@implementation MyAppLocationUpdater
-(void)getDirectionsFromHere:(CLLocation *)newLocation
{
  // same stuff as before except
  if (viewController != nil) {
    [viewController performSelectorOnMainThread:@selector(updateDirectionsWithHtml:) withObject:directionsOutput waitUntilDone:NO];
  }
  // release pool as before
}

@end

It might simplify things to use NSOperation and NSOperationQueue. In this case you'd set up one operation to create the HTML string and a second operation that depends on the first and puts the HTML into the web view. When the view controller is deallocating you should cancel that second operation. Keep in mind that still the first operation cannot retain any references to the view controller.


Have you tried turning on Zombie checking? You say "[not releasing objects in dealloc] isn't a solution, because as far as I can tell I'm just leaking those two objects now", but it sure describes like you're sending release to an already released object.

One way to check for this is to override alloc/dealloc retain/release in your trouble objects and log those events. Then you can see where each is getting called, and perhaps discover the heretofore unknown release.


I experience also that problem when I hit the back button too early. What I did is I add a [self retain] in the body of the function that will be called on a separate thread and in the viewDidUnload I add [self release]. Simple. Hope this helps also.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜