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 в этой точке.