Обновление освобожденного UIWebView из фонового потока

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

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

В подклассе 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, но это не помогло.

(Между прочим, API-интерфейсы 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... и теперь у меня не происходит сбой. Это не решение, потому что, насколько я могу судить, сейчас я просто сливаю эти два объекта, но это, безусловно, интересно. Или, по крайней мере, я надеюсь, что это для кого-то, потому что я в полной растерянности.


person Dan Ray    schedule 07.06.2010    source источник
comment
Как упоминалось в моем обновлении ниже, я почти уверен, что ваша проблема на самом деле находится немного дальше по трассировке стека: ваш контроллер освобождается, когда он освобождается фоновым потоком по завершении.   -  person walkytalky    schedule 08.06.2010
comment
Еще раз, как указано ниже: поток завершается (#17-#12), освобождает контроллер (#11), счетчик сохранения становится равным 0, контроллер освобождается (#10), материал UIKit освобождается по очереди (#8-#2). ), бум (#0).   -  person walkytalky    schedule 08.06.2010
comment
Я действительно не хочу спорить с кем-то, кто так полезен, но... Эта трассировка возникла в результате вызова моего метода Dealloc, который произошел, когда я нажал кнопку «Назад» на панели навигации. Я не понимаю, как это может быть запущено в фоновом режиме. Но погодите: вы начали с того, что подшучивали надо мной, и оказалось, что вы были совершенно правы, так что я возьму свою очередь подшутить над вами, и, может быть, вы снова окажетесь совершенно правы. Какого черта фоновый поток освобождает контроллер представления, членом которого он является? И что с этим можно сделать?   -  person Dan Ray    schedule 08.06.2010
comment
Хорошо, надеюсь, еще одно обновление должно объяснить мои рассуждения. У меня пока нет четкого представления, что с этим делать. Хотя, возможно, стоит подумать о том, чтобы применить метод делегата к другому объекту.   -  person walkytalky    schedule 08.06.2010


Ответы (6)


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

Если проблема заключалась в доступе к веб-представлению после освобождения в ответ на последовательность вызовов, показанную выше, тогда этот вызов будет исходить от 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? Или иначе, есть ли что-то еще, что может привести к преждевременному завершению потока?

person walkytalky    schedule 07.06.2010
comment
Я слышу вас громко и ясно обо всем этом @walkytalky. Чего я не сделал, так это явно не установил для делегата моего менеджера значение nil, поэтому позвольте мне попробовать это, просто чтобы понять, в чем, по моему мнению, проблема. А пока есть какие-нибудь мысли о полной повторяемости этого крушения? У меня есть регистрация в моих методах делегата, которые показывают меня в консоли, когда диспетчер местоположения обновляет меня, и ясно, что если я нажму кнопку «Назад» моего navController до того, как это произойдет, мы взорвемся. Я не понимаю, как это может иметь какое-либо отношение к фоновым потокам. - person Dan Ray; 08.06.2010
comment
Кстати, я предполагаю, что во фразе Tried to obtain the web lock сеть относится к моему UIWebView. Я хочу сделать это предположение явным, потому что на самом деле я не ЗНАЮ, что это тот элемент моей точки зрения, о котором идет речь в этом сообщении. - person Dan Ray; 08.06.2010
comment
Моя единственная мысль о повторяемости заключается в том, что она должна облегчить поиск проблемы, но я понимаю, что сейчас это может быть не очень утешительным. И еще одно: если вы поместите вызов getDirectionsFromHere в основной поток — или вообще удалите его — проблема все равно возникнет? - person walkytalky; 08.06.2010
comment
Пробовал. Если я оставлю getDirectionsFromHere на переднем плане, то пользовательский интерфейс зависнет в критический момент для этой ошибки. Я не могу нажать кнопку «Назад» в течение периода, который может вызвать сбой. Но возникли более странные вещи; см. правку на ОП. - person Dan Ray; 08.06.2010
comment
Спасибо за ваше обновление. У меня есть точка останова в моем методе Dealloc, и я почти уверен, что он вызывается только один раз и только при резервном копировании на панели навигации. И да, я доработал свое окно этой ошибки, это определенно происходит, только если я освобождаю контроллер представления, в то время как мой фоновый поток ожидает своего сетевого ответа, хотя сбой происходит не там, а в делелоке. - person Dan Ray; 08.06.2010
comment
Dealloc всегда должен вызываться только один раз, в ответ на то, что счетчик сохранения достигает 0 - повторно вызывается выпуск, и в трассировке стека определенно похоже, что решающий происходит при завершении потока. - person walkytalky; 08.06.2010

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

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

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

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

person Mike Weller    schedule 08.06.2010
comment
Спасибо, но я немного уточнил свой вопрос после той части, на которую вы отвечаете. Взгляните на две мои поправки. (Кстати, я вернусь к началу и отмечу, что OP перед правками — это что-то вроде гусиной погони. Извините за это.) - person Dan Ray; 08.06.2010

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

[pool drain];
[pool release];

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

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

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

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

person Mike Weller    schedule 08.06.2010
comment
Спасибо за это, я думал, что должен был сделать и то, и другое. Сейчас я исправил это, но это не решило сбой. - person Dan Ray; 08.06.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 в веб-представление. Когда контроллер представления освобождается, вы должны отменить эту вторую операцию. Имейте в виду, что по-прежнему первая операция не может сохранять никаких ссылок на контроллер представления.

person Geoff Reedy    schedule 08.06.2010

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

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

person Olie    schedule 08.06.2010

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

person capecrawler    schedule 14.10.2010