从后台线程更新解除分配的UIWebView

时间:2010-06-07 20:54:46

标签: iphone memory-management core-location

(两个编辑跟随这个问题的原始主体,两者都非常彻底地修改了这个问题。不要挂在第一部分 - 虽然有用于上下文目的,但我已经排除了我问的原始问题。)

正如你从标题中看到的那样,我已经把自己编程到一个角落,我有几件事情对我不利......

在UIViewController子类中管理大而复杂的视图。其中一部分是UIWebView,它包含我必须构建和执行的Web请求的输出,并从中手动汇编HTML。由于运行需要一两秒钟,因此我通过调用self performSelectorInBackground:将其放入后台。然后从我调用那个方法,我使用self performSelectorOnMainThread:返回到线程堆栈的表面,用我刚刚得到的更新UIWebView。

像这样(我已经减少只显示相关问题):

-(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];
}

这一切都非常好,除非我在CLLocationManager命中它的委托方法之前退出了这个视图控制器。如果在我离开此视图后发生这种情况,我会得到:

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...

尽管如此,但是当我太早退出时,我可以重复地导致这次崩溃。我完全不相信从后台线程尝试UI更新是真正的问题;我认为我的UIWebView已经解除分配。我怀疑我只是在后台线程这一事实让运行时怀疑某些事情,但我觉得不是这样。

那么当我退出该视图时,如何告诉CLLocationManager不要担心它?我在viewWillDisappear方法中尝试了[self.locationManager stopUpdatingLocation],但是没有这样做。

(顺便提一下,MapQuest的apis非常棒。比Google提供的任何东西都好。我不能高度推荐它们。)

编辑1:

这只是更奇怪了。我实际上已经隔离了崩溃的地方,它位于-(void) dealloc的底部。如果我评论[super dealloc],我不会崩溃!但我似乎无法“进入”[super dealloc]看到超类中发生了什么,它只是崩溃并停止与我交谈。

这是一个堆栈跟踪:

#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 ?? ()

此外,现在很清楚,当[self getDirectionsFromHere]正在运行时触发后退按钮时会发生错误。我们正在等待stringWithContentsOfURL调用返回。

编辑2:

好的,所以注意到该堆栈顶部附近的[UITextView dealloc],我意识到我在这个视图层次结构上只有一个UITextView,所以我在-(void) dealloc中注释掉了该UITextView对象的发布。接下来的崩溃,在跟踪点上有[UIWebView dealloc]。哼!变化!所以我在dealloc方法中注释掉了我的一个webView版本......现在我没有崩溃。这不是一个解决方案,因为据我所知,我现在只是泄漏这两个对象,但它确实很有趣。或者至少,我希望是对某人,因为我完全不知所措。

6 个答案:

答案 0 :(得分:1)

我认为你的诊断可能是错误的,但让我们想象一下它不是,看看你会对它做些什么。

如果问题是在解除分配后访问的Web视图,为了响应上面显示的调用序列,那么该调用将来自updateDirectionsWithHtml: self.directionsWebView中的引用。在这种情况下,您应该确保不要保持陈旧指针,而是将其设置为nil。但是,我认为你已经这样做是公平的,所以这不太可能是问题所在。

如果问题是在解除分配后访问控制器,由位置管理器调用委托方法,那么您应该确保先断开链接。像[locationManager setDelegate:nil]之类的东西应该停止不必要的呼叫,即使位置管理器本身比你的控制器更长。但同样,我怀疑你已经在控制器死之前释放了位置管理器,这也不是问题。

在这种情况下,问题很可能在其他地方,并且可能值得考虑面值的错误消息:是否有可能 从某个后台线程调用UIKit?说运行时对此感到困惑,因为你之前的后台线程对我来说只是一个小问题。这个错误看起来像来自某个相当具体的地方。那个地方可能是错误的,但通常最好先假设操作系统比自己的代码更好。

如果没有看到其余的代码,很难知道这个真正的问题在哪里,如果确实存在的话,可能就是这样。让人想起的一种可能性是,你可能会对某个属性进行一些无辜的使用,而这个属性实际上已经在某个地方触及了UI;但我承认这只是空闲的猜测。

<强>更新

如果将后台调用放到前台会阻止您按下后退按钮,那么耗时的操作必须已经在进行中,这意味着您的委托方法必须已经被调用 - 日志记录告诉您所以只是'通过。

我在这里遇到的一件事是performSelectorInBackgroundperformSelectorOnMainThread都保留了接收器,直到选择器运行完毕。因此,控制器释放的时间可能因此被搞砸了(除非你在某个地方通过过度释放强制它)。

从堆栈跟踪中,它几乎看起来好像dealloc实际上是从后台线程中调用的,作为其自身清理的一部分。然后调用各种UIKit事件,这将解释线程锁定错误。但是如果调用performSelectorOnMainThread那么应该延长控制器的生命周期,直到它回到主线程上。那么也许 毕竟是过度释放?

更新2:

这个讨论有点分散,但让我试着总结一下我的想法:

  • 当您致电[self performSelectorInBackground:...]时,会创建一个新主题并通过主题self发送retain条消息
  • 稍后,您点击后退按钮会导致控制器发送release。但是,因为它是由后台线程保留的,所以它还没有被释放。
  • 后台线程以这种或那种方式完成,此时它向控制器发送release消息。控制器的保留计数降至0,并调用其dealloc方法。但由于此消息来自后台线程,当它尝试执行UI内容(删除子视图)时,会出现线程锁定错误。

在最后一点的理由中,我理解这看起来有点奇怪,请注意崩溃中的调用堆栈显示来自线程完成的调用,而不是来自与后退按钮相关的任何事件处理。

现在,这里有趣的问题不是后台线程发送release的原因,这是其生命周期的一个自然部分,但为什么这会将保留计数减少到0.如果线程实际上是远至performSelectorOnMainThread,应该还有另一条retain消息,并且你的控制器应该存活足够长时间以释放主线程,这应该没问题。

在我看来,有两个可能的原因:要么是线程提前退出,要么是过度释放。但如果是后者,那么在线程终止之前,你会期望self已经命中dealloc

所以我想问题是,updateDirectionsWithHtml是否会在不调用performSelectorOnMainThread的情况下退出?或者,是否有其他东西会导致线程过早终止?

答案 1 :(得分:1)

您已将自己设置为CLLocationManager上的委托。代表几乎总是被实现为弱引用,这意味着它们不会被保留。这意味着当您的视图控制器弹出导航堆栈并释放时,CLLocationManager现在有一个错误的指针,您将遇到崩溃和各种可怕的难以调试的问题。

标准做法是在CLLocationManager方法中将dealloc的委托设置为nil,从而确保在您离开后不会试图给您打电话。即:

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

据我所知,performSelectorInBackground等代码没问题,所有这些调用都会保留接收器,直到选择器返回,所以我不认为这是你问题的根源。

答案 2 :(得分:1)

好的,我会将此作为另一个答案发布,因为我之前的答案仍然有效。您过度释放了自动释放池。

[pool drain];
[pool release];

排水与释放相同。来自documentation

  

在引用计数环境中,此方法的行为与发布相同。由于无法保留自动释放池(请参阅保留),因此会导致接收器被解除分配。

你应该只调用释放或消耗,而不是两者。过度释放池将导致双重免费且随机难以调试的问题。

您可能需要尝试NSZombieEnabled进一步诊断。

答案 3 :(得分:1)

问题是最终版本(因此)dealloc是在主线程以外的线程上调用的。有几种方法可以处理这种情况。一种是对具有对视图控制器的弱引用的不同对象进行后台处理。它可能看起来像:

@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

可能会简化使用NSOperation和NSOperationQueue的事情。在这种情况下,您将设置一个操作来创建HTML字符串,第二个操作依赖于第一个操作并将HTML放入Web视图中。当视图控制器取消分配时,您应该取消第二个操作。请记住,第一个操作仍然无法保留对视图控制器的任何引用。

答案 4 :(得分:0)

你试过打开僵尸检查吗?你说“[不释放dealloc中的对象]不是一个解决方案,因为据我所知,我现在只是泄漏这两个对象”,但它确实描述了你正在向已经发布的对象发送释放。

检查此问题的一种方法是覆盖故障对象中的alloc / dealloc retain / release并记录这些事件。然后你可以看到每个被调用的地方,也许还可以发现迄今未知的版本。

答案 5 :(得分:0)

当我太早点击后退按钮时,我也遇到了这个问题。 我所做的是在函数体中添加一个[self retain],它将在一个单独的线程上调用,并在viewDidUnload中添加[self release]。简单。希望这也有帮助。