Обновление освобожденного UIWebView из фонового потока - PullRequest
1 голос
/ 08 июня 2010

(Две редакции следуют за исходной частью этого вопроса, обе из них довольно радикально модифицируют вопрос. Не зацикливайтесь на первой части этого - хотя это полезно для контекстных целей, я почти исключил исходная проблема, о которой я спрашивал.)

Как видно из названия, я запрограммировал себя в угол, и у меня есть несколько вещей, которые работают против меня ...

В подклассе UIViewController, который управляет большим и сложным представлением. Одной из его частей является UIWebView, который содержит выходные данные веб-запроса, который я должен был создать и выполнить, а также вручную собрать 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...

Несмотря на то, что это говорит, я могу неоднократно вызывать этот сбой, когда ухожу слишком рано. Я нисколько не убежден, что попытка обновления пользовательского интерфейса из фонового потока - действительно проблема; Я думаю, это то, что мой UIWebView освобожден. Я подозреваю, что тот факт, что я был просто в фоновом потоке, заставляет среду выполнения подозревать, что с этим что-то не так, но я уверен, что это не так.

Итак, как мне сказать CLLocationManager не беспокоиться об этом, когда я отказываюсь от этого представления? Я попытался [self.locationManager stopUpdatingLocation] внутри моего метода viewWillDisappear, но это не сработало.

(Между прочим, apis MapQuest ФАНТАСТИЧЕСКИ. Гораздо лучше, чем все, что предоставляет 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 в этой иерархии представлений, поэтому я прокомментировал выпуск этого объекта UITextView в -(void) dealloc. Следующий сбой был [UIWebView dealloc] в этом месте следа. Гм! Перемена! Поэтому я прокомментировал выпуск моего единственного webView в методе dealloc ... и теперь у меня нет сбоев. Это не решение, потому что, насколько я могу судить, я сейчас просто сливаю эти два объекта, но это, безусловно, интересно. Или, по крайней мере, я надеюсь, что это кому-то, потому что я в полном недоумении.

Ответы [ 6 ]

1 голос
/ 08 июня 2010

Проблема в том, что окончательный выпуск (и, следовательно,) 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 в веб-представление. Когда контроллер представления освобождается, вы должны отменить эту вторую операцию. Имейте в виду, что первая операция не может сохранять никаких ссылок на контроллер представления.

1 голос
/ 08 июня 2010

ОК, я опубликую это как другой ответ, так как мой предыдущий все еще действителен. Вы перевыпускаете пул автоматического выпуска.

[pool drain];
[pool release];

сток идентичен выпуску. Из документации :

В среде с подсчетом ссылок этот метод ведет себя так же, как и выпуск. Поскольку пул автоматического выпуска не может быть сохранен (см. Сохранение), это приводит к тому, что получатель освобождается.

Вы должны вызывать только релиз или слив, а не оба Чрезмерное освобождение пула приведет к двойным свободным и снова случайным трудным для отладки проблемам.

Возможно, вы захотите попробовать NSZombieEnabled для дальнейшей диагностики этого.

1 голос
/ 08 июня 2010

Вы указали себя в качестве делегата на CLLocationManager. Делегаты почти всегда используются как слабые ссылки, то есть они не сохраняются. Это означает, что когда ваш контроллер представления извлечен из стека навигации и выпущен, CLLocationManager теперь имеет неверный указатель, и вы получите сбои и всевозможные ужасные проблемы, которые трудно отладить.

Стандартная практика состоит в том, чтобы установить для делегата CLLocationManager значение nil в вашем методе dealloc, таким образом, убедившись, что он не будет пытаться вызвать вас после вашего ухода. то есть:

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

Насколько я могу сказать, ваш performSelectorInBackground и т. Д. Код в порядке, все эти вызовы сохраняют получателя до тех пор, пока селектор не вернется, поэтому я не думаю, что это является источником ваших проблем.

1 голос
/ 08 июня 2010

Я думаю, что ваш диагноз, вероятно, неверный, но давайте представим на данный момент, что это не так, и посмотрим, что вы можете с этим поделать.

Если проблема заключалась в доступе к веб-представлению после освобождения в ответ на последовательность вызовов, показанную выше, то этот вызов будет поступать из updateDirectionsWithHtml: через ссылку в self.directionsWebView. В этом случае вы должны просто убедиться, что не держите устаревший указатель, а установите его на nil. Тем не менее, я думаю, это справедливая ставка, что вы делаете уже делаете это, так что вряд ли это проблема.

Если проблема была в том, что к контроллеру обращались после освобождения, диспетчер местоположений вызвал метод делегата, то сначала вы должны разорвать связь. Что-то вроде [locationManager setDelegate:nil] должно остановить нежелательный вызов, даже если диспетчер местоположений сам переживает ваш контроллер. Но опять же, я подозреваю, что вы уже выпускаете диспетчер местоположений до того, как контроллер умрет, и это тоже не проблема.

В этом случае проблема наиболее вероятна в другом месте, и, возможно, стоило бы рассмотреть сообщение об ошибке по номиналу: возможно ли, что где-то вы вызываете UIKit из фонового потока? Сказать, что среда выполнения смущена этим, потому что вы были в фоновом потоке ранее, кажется мне немного сомнительным. Эта ошибка выглядит так, как будто она исходит из чего-то конкретного. Это место может содержать ошибки, но обычно лучше начинать с предположения, что ОС работает лучше, чем собственный код.

Не видя остальную часть кода, трудно понять, где может быть настоящая проблема, если она действительно существует. Одна возможность, которая приходит на ум, заключается в том, что вы можете использовать невинно выглядящее свойство, которое на самом деле оказывается где-то в интерфейсе пользователя; но я признаю, что это просто пустые предположения.

Обновление:

Если перевод фонового вызова на передний план не позволяет вам нажать кнопку «назад», то операция, отнимающая много времени, уже должна выполняться, а это значит, что ваш метод делегата уже был вызван - запись в журнал сообщает вам, что просто нет не проходит.

Здесь мне приходит в голову то, что и performSelectorInBackground, и performSelectorOnMainThread сохраняют свой приемник до тех пор, пока селектор не завершит работу. Таким образом, время освобождения вашего контроллера, вероятно, искажено в результате (если вы не форсируете его из-за чрезмерного выпуска).

Из вашей трассировки стека это выглядит так, как будто dealloc фактически вызывается из фонового потока как часть его собственной очистки. Затем он обращается к различным UIKit-объектам, что объясняет ошибку блокировки потока. Но если вызывается performSelectorOnMainThread, это должно продлить срок службы контроллера, пока он не вернется в основной поток. Так, может быть, - это, в конце концов, переизбыток?

Обновление 2:

Эта дискуссия немного разбросана, но позвольте мне попытаться подвести итог тому, что, по моему мнению, происходит:

  • Когда вы звоните [self performSelectorInBackground:...], создается новый поток, и self получает сообщение retain потоком .
  • Через некоторое время вы нажимаете кнопку "Назад" и заставляете ваш контроллер отправлять release. Однако, поскольку он был сохранен фоновым потоком, он еще не освобожден.
  • Так или иначе фоновый поток завершает свою работу, после чего он отправляет release сообщение контроллеру. Число сохраняемых контроллеров падает до 0, и вызывается его метод dealloc. Но так как это сообщение приходит из фонового потока, при попытке выполнить пользовательский интерфейс (удаление подпредставлений) вы получаете ошибку блокировки потока.

В обоснование этого последнего бита, который, как я понимаю, может показаться немного странным, обратите внимание, что в стеке вызовов после сбоя показаны вызовы, поступающие с завершения потока, а не с какой-либо обработкой событий, связанной с вашей кнопкой возврата.

Теперь интересный вопрос заключается не в том, почему фоновый поток отправляет release, что является естественной частью его жизненного цикла, а в том, почему это уменьшает количество сохраняемых данных до 0. Если поток фактически достигает performSelectorOnMainThread, должно быть еще одно retain сообщение, и ваш контроллер должен выжить достаточно долго, чтобы освободить его в главном потоке, что должно быть хорошо.

Мне кажется, есть две возможные причины: либо преждевременный выход из потока, либо его чрезмерное освобождение. Но если бы он был последним, вы бы ожидали, что self уже достигнет dealloc до завершения потока.

Итак, я думаю, вопрос в том, может ли updateDirectionsWithHtml выйти без вызова performSelectorOnMainThread? Или, в противном случае, существует ли что-то еще, что может привести к преждевременному завершению потока?

0 голосов
/ 14 октября 2010

Я также испытываю эту проблему, когда слишком рано нажимаю кнопку "Назад". Я добавил [self retain] в тело функции, которая будет вызываться в отдельном потоке, а в viewDidUnload я добавил [self release]. Просто. Надеюсь, что это также помогает.

0 голосов
/ 08 июня 2010

Вы пытались включить проверку Зомби?Вы говорите: «[не выпускать объекты в dealloc] - это не решение, потому что, насколько я могу судить, я сейчас просто сливаю эти два объекта», но это точно описывает, как вы отправляете релиз уже выпущенному объекту.

Один из способов проверить это - переопределить alloc / dealloc retain / release в ваших проблемных объектах и ​​зарегистрировать эти события.Затем вы можете увидеть, куда звонит каждый, и, возможно, обнаружить ранее неизвестный выпуск.

...