Dealloc вызывается в фоновом режиме сбойной очереди GCD, созданной с помощью ARC
У меня есть контроллер представления, который загружает актив в фоновом режиме GCD. Я передаю свою загрузочную функцию блоку обратного вызова для выполнения после завершения загрузки и всегда выполняет этот блок в основном потоке.
Проблема возникает, если мой контроллер просмотра уволен пользователем до завершения загрузки. Я подозреваю, что происходит, когда мой диспетчер просмотров отклонен, блок обратного вызова - единственное, что сохраняет сильную ссылку на контроллер. Блок обратного вызова сохраняется только в фоновом потоке, поэтому после его освобождения все объекты, захваченные в области блока обратного вызова, также освобождаются, хотя и в фоновом режиме.
Это проблема: освобождение в фоновом режиме приводит к тому, что dealloc запускается в той же очереди, а не в главной очереди. Это, в свою очередь, вызывает dealloc
в фоновом режиме и приложение сбой:
2012-01-19 12:47:36.349 500px iOS[4892:12107] bool _WebTryThreadLock(bool), 0x306c10: 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...
[Switching to process 16643 thread 0x4103]
[Switching to process 16643 thread 0x4103]
(gdb) where
#0 0x307fd3c8 in _WebTryThreadLock ()
#1 0x307ff1b0 in WebThreadLock ()
#2 0x33f7865e in -[UITextView dealloc] ()
#3 0x0005d2ce in -[GCPlaceholderTextView dealloc] (self=0x309010, _cmd=0x340ce6b8) at /Users/ash/Dropbox/500px/500px-ios/500px iOS/500px iOS/GCPlaceholderTextView.m:113
#4 0x337cac42 in -[NSObject(NSObject) release] ()
#5 0x33dee5f4 in -[UIView(Hierarchy) removeFromSuperview] ()
#6 0x33e836cc in -[UIScrollView removeFromSuperview] ()
#7 0x33f762f0 in -[UITextView removeFromSuperview] ()
#8 0x33e01de2 in -[UIView dealloc] ()
#9 0x337cac42 in -[NSObject(NSObject) release] ()
#10 0x33dee5f4 in -[UIView(Hierarchy) removeFromSuperview] ()
#11 0x33e01de2 in -[UIView dealloc] ()
#12 0x33f437e4 in -[UIScrollView dealloc] ()
#13 0x337cac42 in -[NSObject(NSObject) release] ()
#14 0x33dee5f4 in -[UIView(Hierarchy) removeFromSuperview] ()
#15 0x33e836cc in -[UIScrollView removeFromSuperview] ()
#16 0x33e01de2 in -[UIView dealloc] ()
#17 0x337cac42 in -[NSObject(NSObject) release] ()
#18 0x33dee5f4 in -[UIView(Hierarchy) removeFromSuperview] ()
#19 0x33e01de2 in -[UIView dealloc] ()
#20 0x337cac42 in -[NSObject(NSObject) release] ()
#21 0x33e5a00e in -[UIViewController dealloc] ()
#22 0x00035f16 in -[PXPhotoViewController dealloc] (self=0x5158d0, _cmd=0x340ce6b8) at /Users/ash/Dropbox/500px/500px-ios/500px iOS/500px iOS/PXPhotoViewController.m:118
#23 0x337cac42 in -[NSObject(NSObject) release] ()
#24 0x337e5046 in sendRelease ()
#25 0x331fc92e in _Block_object_dispose ()
#26 0x0003c33a in __destroy_helper_block_ () at /Users/ash/Dropbox/500px/500px-ios/500px iOS/500px iOS/PXPhotoViewController.m:878
#27 0x331fc88e in _Block_release ()
#28 0x331fc91c in _Block_object_dispose ()
#29 0x000c8d32 in __destroy_helper_block_ () at /Users/ash/Dropbox/500px/500px-ios/500px iOS/500px iOS/PXPhotoFetcher.m:557
#30 0x331fc88e in _Block_release ()
#31 0x35eec8ec in _dispatch_call_block_and_release ()
#32 0x35ee2de2 in _dispatch_queue_drain ()
#33 0x35ee2f32 in _dispatch_queue_invoke ()
#34 0x35ee24f2 in _dispatch_worker_thread2 ()
#35 0x34ecb590 in _pthread_wqthread ()
#36 0x34ecbbc4 in start_wqthread ()
Код, который я выполняю в основном потоке, выглядит следующим образом:
[[PXPhotoFetcher sharedPXPhotoFetcher] fetchPhotoDetailsWithPriority:PhotoRequestLowPriority
withCallback:^(PXPhotoModel *thePhotoModel) {
// a callback function which captures self in its scope
} forModel:model];
Я создаю для 4.3, поэтому, если я использую ссылку __unsafe_unretained для self в блоке обратного вызова, это исправит мою текущую проблему, но представит новую проблему наличия висячего указателя.
Что вы рекомендуете для архитектурного решения для этого? Другие обходные пути включали переопределение release
, так что он всегда вызывался в основном потоке, но это явно не будет работать в среде ARC.
Кажется, что это должна быть гораздо более распространенная проблема с ARC и GCD, но я ничего не могу найти в Интернете. Это просто потому, что я нацеливаюсь < iOS 5 и не может использовать слабые ссылки?
Ответы
Ответ 1
UPDATE: приведенное ниже решение фактически не работало на iOS 4. По какой-то причине оно работало по 5, но не по 4, поэтому я придумал лучшее решение.
Проблема вызвана уничтожением блока в фоновом режиме, поэтому я помещаю его в локальную переменную и в фоновом блоке вызывают его, а затем асинхронно передают его блоку основного потока, чтобы он был выпущен там. Это также беспорядочно, но выглядит следующим образом:
void(^block)(void) = ^{/*do all the things*/};
dispatch_async(queue, ^{
block();
dispatch_async(dispatch_get_main_queue(), ^{
if ([block isKindOfClass:[NSString class]])
NSLog(@"Whoa, bro");
});
});
Код в основном потоке - всего лишь трюк, чтобы убедиться, что компилятор не просто полностью оптимизирует код; Мне нужно, чтобы объект блока был выпущен последним по основному потоку. Этот код работает с уровнем оптимизации компилятора -Os
.
Итак, я придумал решение своей проблемы, хотя это супер хаки. Я не смог воспроизвести проблему с этого исправления, хотя я думаю, что это очень плохой архитектурный проект.
Чтобы повторить, проблема в том, что у меня есть блок в очереди фона, который уничтожается. Этот блок является объектом, и ему принадлежит сильная ссылка на блок обратного вызова, который содержит сильную ссылку на self. Блок фона освобождается из фоновой очереди. Так что я сделал, это перенос вызова в другой диспетчерский вызов в основную очередь. Таким образом, мой метод извлечения:
dispatch_async(backgroundQueue, ^{
/* do all the things */
});
Для этого:
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_async(backgroundQueue, ^{
/* do all the things */
});
});
Код асинхронно отправляет блок в основную очередь, который затем отправляет блок в очередь фона. Поскольку фоновый блок является объектом и относится к области основной очереди, он выдается в основном потоке, в результате чего происходит дезактивация UITextView, приводящая к сбою в главной очереди, а также решение моей проблемы.
Очевидным архитектурным решением является использование ссылки __weak
для self в моем блоке обратного вызова, но мне придется подождать, пока я откажу поддержку iOS 4.3.
Ответ 2
Фактически, GCD позволяет сохранять и освобождать для очередей отправки для этой цели. Он фактически документируется по адресу: "Управление памятью для очередей отправки" .
dispatch_retain(first_queue);
dispatch_async(a_queue, ^{
do_not_wait_for_me();
dispatch_async(first_queue, ^{ i_am_done_now(); });
dispatch_release(first_queue);
});
В вашем сценарии я бы заменил first_queue на вашу основную очередь отправки. Сохраняя основную очередь отправки, вы убедитесь, что она не освобождается до завершения обратного вызова.
Другой пример можно найти по адресу: " Выполнение блока завершения при выполнении задачи."
Ответ 3
Зола,
Ваша проблема заключается в раннем освобождении, поскольку ваши очереди срываются. Следовательно, перестаньте это делать. Как? Ты почти там. Из вашей фоновой очереди вы хотите, чтобы SYNCHRONOUSLY возвращали данные контроллеру в основном потоке. Таким образом, когда все раскручивается, они освобождаются в безопасном порядке. Например:
dispatch_async(backgroundQueue, ^{
/* download stuff */
dispatch_sync(dispatch_get_main_queue(), ^{
// Pass the data to the controller.
});
});
Этот шаблон позволяет гарантировать правильную доставку ваших данных в любой компонент пользовательского интерфейса в основном потоке, а затем позволяет изящный выпуск.
Эндрю
Изменить второй ответ с помощью переменной __block:
__block UIViewController *vc = danglingVC;
dispatch_async(backgroundQueue, ^{
/* download stuff */
dispatch_async(dispatch_get_main_queue(), ^{
// Pass the data to the controller.
vc = nil;
});
});
С помощью ARC вы принудительно освобождаете установки, устанавливая прочный слот для хранения в нуль. Обратите внимание, что теперь я могу сделать передачу в основной поток асинхронной. Я думаю, что это четкое и ясное решение вашей проблемы. Это не зависит от тонких взаимодействий между ARC, GCD и блоками.
Эндрю
Ответ 4
Ваш анализ кажется звуковым. Поэтому, возможно, конец блока может сохранить контроллер и сделать авторекламу на основном потоке (возможно, после небольшой задержки, чтобы избежать условий гонки).
Ответ 5
Вы никогда не можете гарантировать, что объект будет освобожден от любого конкретного потока, как вы узнали.
Лучшее решение, которое я нашел, находится в вашем dealloc, проверьте, находитесь ли вы в основном потоке. Если нет, выполнитеSelector в основном потоке с остальной частью dealloc и дождитесь завершения.
В качестве альтернативы, реализуйте ту же логику с блоками: если не основной поток, dispatch_sync в основную очередь с остальной частью dealloc.
Ответ 6
Альтернативой будет сохранение ссылки объекта в области вне асинхронной отправки. Например:
// create a dispatch queue
dispatch_queue_t sdq = dispatch_queue_create("com.fred.exampleQueue", NULL);
// my object references container
__block NSMutableArray *ressources = [[NSMutableArray alloc] init];
dispatch_async(sdq, ^{
__block MyLoader *ml = [[MyAsyncLoader alloc] initWithCallback:^(id result)
{
NSLog(@"loader result: %@", result);
// since I ask for ressource in my final CallBack it still here
// and my loader too, I can now dispose of it.
[ressources removeObject:ml];
}];
// perform async loading and call my callback when it done...
[ml asyncLoad];
// save my object
[ressources addObject:ml];
});
Ответ 7
Недавно я столкнулся с подобной проблемой. У меня была роскошь использовать обнуление слабых ссылок в ARC для решения этой конкретной проблемы. Однако я рассматривал альтернативное решение, которое должно работать с MRC.
__block UIViewController *vc = ...;
[vc retain];
dispatch_async(backgroundQueue, ^{
// ... do some things with vc
dispatch_async(dispatch_get_main_queue(), ^{
// ... do more things with vc
[vc release]; // vc might be dealloc'd here but not before
});
});
Квалификатор хранения __block
на vc
гарантирует, что блок не сохранит объект, на который ссылается vc
. Контроллер представления сохраняется до тех пор, пока блок, который вызывается в основной очереди, не освободит его и, возможно, получит dealloc'd в этой точке.