Сбой CATiledLayer в iOS

У меня есть приложение для чтения PDF-файлов для iPad, где я использую прокрутку для отображения каждой страницы. Я держу страницу в поле зрения и одну страницу с каждой стороны страницы в поле зрения. У меня есть отдельные представления для портретной и альбомной ориентации. В книжной ориентации отображается одна страница, а в альбомной — 2 страницы.

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

PDF-файлы нарисованы с использованием tiledlayers. Приложение вылетает при изменении ориентации больших PDF-файлов. Приложение аварийно завершает работу только в том случае, если ориентация изменяется до того, как все плитки будут нарисованы. Я предполагаю, что он дает сбой, потому что он пытается нарисовать плитки в представлении, которое было выгружено. Так есть ли способ остановить отрисовку тайлов, когда я выгружаю вид?


person booboo-a-choo    schedule 16.05.2011    source источник
comment
Я не нашел подходящего решения для этого, но обходной путь предотвратил сбои. Закончилось помещением if (self.tiling == YES && [self.view superview] != nil) в метод - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx. Саморазбиение устанавливается перед удалением вида из супервизора или, в вашем случае, перед изменением ориентации. Вы можете избежать этой проблемы, имея один вид для всех ориентаций. Мой обходной путь предотвратил сбои, но не помешал CATiledLayer получить доступ к изображению или удерживать блокировку изображения каким-то таинственным образом. Конечным результатом была невозможность удалить указанное изображение из файловой системы. Удачи!   -  person TheBlack    schedule 16.05.2011


Ответы (2)


Вам нужно установить для делегата CALayer значение nil, а затем удалить его из супервизора. Это останавливает рендеринг, после чего вы можете безопасно освободить место.

- (void)stopTiledRenderingAndRemoveFromSuperlayer; {
    ((CATiledLayer *)[self layer]).delegate = nil;    
    [self removeFromSuperview];
    [self.layer removeFromSuperlayer];
}

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

person steipete    schedule 01.08.2011
comment
1) Вы уверены, что [self.layer removeFromSuperlayer]; требуется? 2) Разве тот факт, что ваше сохранение/освобождение помогает предотвратить сбои, не указывает на то, что у вас есть состояние гонки, которое все еще может произойти? Сохранение защищает от вызова dealloc (в результате освобождения в другом потоке), но освобождение все еще может произойти между запуском функции и вызовом сохранения, и в конечном итоге вы вызовете сохранение для освобожденной памяти. - person Danra; 14.11.2011
comment
Я также обнаружил, что вы также должны удалить все подпредставления до установки self.layer.delegate nil, иначе они не будут освобождены. - person Danra; 14.11.2011
comment
Вы правы, сохранение/освобождение не требуется - я обновил свой ответ. - person steipete; 15.11.2011
comment
Вы, сэр, прекрасны (не говоря уже о красоте PSPDFKit =P). - person fumoboy007; 02.04.2013

Я не смотрел разборку, чтобы увидеть, но мы используем немного другое решение. Установка свойства CATiledLayer.content в nil блоков и принудительное завершение всех блоков рендеринга в очереди. Это можно безопасно запустить в фоновом потоке, а затем выпустить UIView можно вернуться в основной поток, чтобы позволить виду и слою освободиться.

Вот один из примеров реализации UIViewController dealloc, который будет поддерживать ваше CATiledLayer представление живым достаточно долго, чтобы безопасно остановить рендеринг, не блокируя основной поток.

- (void)dealloc
{
    // This works around a bug where the CATiledLayer background drawing 
    // delegate may still have dispatched blocks awaiting rendering after
    // the view hierarchy is dead, causing a message to a zombie object.
    // We'll hold on to tiledView, flush the dispatch queue, 
    // then let go of fastViewer.
    MyTiledView *tiledView = self.tiledView;
    if(tiledView) {
        dispatch_background(^{
            // This blocks while CATiledLayer flushes out its queued render blocks.
            tiledView.layer.contents = nil;

            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                // Make sure tiledView survives until now.
                tiledView.layer.delegate = nil;
            });
        });
    }
}

Это предположение, но некоторые фреймворки/классы Apple (StoreKit, CATiledLayer, UIGestureRecognizer) утверждают, что имеют @property (weak) id delegate реализации, но явно неправильно обрабатывают делегат weak. Глядя на некоторые разборки, они делают явно связанные с гонкой проверки if != nil, а затем напрямую касаются слабого свойства. Правильный способ — объявить __strong Type *delegate = self.delegate, что либо завершится успешно и даст надежную ссылку, гарантированно сохранившуюся, либо будет nil, но конечно не даст вам ссылку на объект-зомби (мой предполагаю, что код фреймворка не был обновлен до ARC).

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

Установка content = nil вызывает dispatch_wait и блокирует до тех пор, пока все существующие блоки рендеринга в очереди не будут завершены. Мы возвращаемся к основному потоку, чтобы убедиться, что dealloc в безопасности.

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

person russbishop    schedule 01.12.2014