Как сохранить NSBitmapImageRep от создания большого количества промежуточных CGImages? - PullRequest
0 голосов
/ 06 октября 2018

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

Я сделал очевидную вещь и перенес вычисления в другой поток, чтобы пользовательский интерфейс мог реагировать.Это помогло, но только немного.Я достиг этого, имея NSBitmapImageRep, в который я обернул NSGraphicsContext, чтобы я мог в него втянуть.Но мне нужно было убедиться, что я не пытаюсь нарисовать его на экране в основном потоке пользовательского интерфейса, в то время как я также рисую в фоновом потоке.Поэтому я ввел замок.Рисование может занять много времени, так как данные также увеличиваются, поэтому даже это было проблематично.

Моя последняя ревизия имеет 2 NSBitmapImageRep с.Один содержит последнюю нарисованную версию и выводится на экран всякий раз, когда необходимо обновить представление.Другой нарисован в фоновом потоке.Когда рисование в фоновом потоке завершено, оно копируется в другой.Я делаю копию, получая базовый адрес каждого и просто вызывая memcpy(), чтобы фактически переместить пиксели от одного к другому.(Я пытался поменять их местами, а не копировать, но, хотя рисование заканчивается вызовом [-NSGraphicsContext flushContext], я получаю частично отрисованные результаты, нарисованные в окне.)

Поток вычисления выглядит следующим образом:

    BOOL    done    = NO;
    while (!done)
    {
        self->model->lockBranches();
        self->model->iterate();
        done = (!self->model->moreToDivide()) || (!self->keepIterating);
        self->model->unlockBranches();

        [self drawIntoOffscreen];

        dispatch_async(dispatch_get_main_queue(), ^{
            self.needsDisplay = YES;
        });
    }

Это работает достаточно хорошо для поддержания отзывчивости интерфейса.Тем не менее, каждый раз, когда я копирую нарисованное изображение в мигающее изображение, я называю [-NSBitmapImageRep baseAddress].Глядя на профиль памяти в инструментах, каждый вызов этой функции приводит к созданию CGImage.Кроме того, CGImage не освобождается до окончания вычислений, что может занять несколько минут.Это приводит к увеличению памяти.Я вижу около 3-4 гигабайт CGImages в моем процессе, хотя мне никогда не нужно больше двух из них.После завершения вычислений и очистки кеша память моего приложения уменьшается до 350-500 МБ.Я не думал использовать пул автоматического выпуска в цикле вычислений для этого, но попробую.

Похоже, что ОС кэширует создаваемые ими образы.Тем не менее, он не очищает кэш, пока вычисления не будут завершены, поэтому он растет без ограничений до тех пор.Есть ли способ предотвратить это?

1 Ответ

0 голосов
/ 06 октября 2018

Не используйте -bitmapData и memcpy() для копирования изображения.Нарисуйте одно изображение в другое.

Я часто рекомендую разработчикам прочитать раздел «NSBitmapImageRep: заметки о соответствии импеданса CoreGraphics и производительность» в 10.6 заметках о выпуске AppKit :

NSBitmapImageRep: Замечания по согласованию импеданса CoreGraphics и производительности

В вышеприведенных примечаниях к выпуску подробно описаны изменения ядра на уровне NSImage для SnowLeopard.Также произошли существенные изменения на уровне NSBitmapImageRep, также для повышения производительности и улучшения согласования импеданса с CoreGraphics.

NSImage - это довольно абстрактное представление изображения.Это в значительной степени просто вещь, которую можно нарисовать, хотя она и менее абстрактна, чем NSView, поскольку она не должна вести себя по-разному в зависимости от аспектов контекста, в которые она втягивается, за исключением качественных решений.Это своего рода непрозрачное утверждение, но его можно проиллюстрировать на примере: если вы рисуете кнопку в области 100x22 по сравнению с областью 22x22, вы можете ожидать, что кнопка будет растягивать свою середину, но не концевые заглавные буквы.Изображение не должно вести себя так (и если вы попробуете это, вы, вероятно, сломаетесь!).Изображение должно всегда линейно и равномерно масштабироваться, чтобы заполнить прямоугольник, в котором оно нарисовано, хотя оно может выбирать представления и тому подобное, чтобы оптимизировать качество для этой области.Аналогично, все представления изображений в NSImage должны представлять один и тот же чертеж.Не упаковывайте какое-либо совершенно другое изображение в качестве представителя.

Это отступление от нас, NSBitmapImageRep - гораздо более конкретный объект.NSImage не имеет пикселей, NSBitmapImageRep делает.NSBitmapImageRep - это фрагмент данных вместе с информацией о формате пикселей и информацией о цветовом пространстве, который позволяет нам интерпретировать данные как прямоугольный массив значений цвета.

Это то же самое, что и CGImage.В SnowLeopard NSBitmapImageRep изначально поддерживается CGImageRef, а не напрямую порцией данных.CGImageRef действительно имеет кусок данных.В то время как в Leopard NSBitmapImageRep, созданный из CGImage, распаковывает и, возможно, обрабатывает данные (что происходит при чтении из растрового файла), в SnowLeopard мы стараемся просто повиснуть на исходном CGImage.

Это имеет некоторыепоследствия производительности.Большинство хороши!Вы должны видеть меньше кодирования и декодирования растровых данных как CGImages.Если вы инициализируете NSImage из файла JPEG, а затем рисуете его в PDF, вы должны получить PDF того же размера, что и исходный JPEG.В Leopard вы увидите PDF размер распакованного изображения.В качестве другого примера, кеши CoreGraphics, включая выгрузку на графическую карту, привязаны к экземплярам CGImage, поэтому чем больше будет использоваться один и тот же экземпляр, тем лучше.

Однако: в некоторой степени быстрые операциис NSBitmapImageRep изменились.CGImages не являются изменяемыми, NSBitmapImageRep является.Если вы измените NSBitmapImageRep, ему, скорее всего, придется скопировать данные из CGImage, включить ваши изменения и упаковать его как новый CGImage.Таким образом, в основном рисование NSBitmapImageRep выполняется быстро, просмотр или изменение его данных пикселей - нет.Это было верно в Leopard, но теперь это более верно.

Вышеуказанные шаги действительно выполняются лениво: если вы делаете что-то, что заставляет NSBitmapImageRep копировать данные из его вспомогательного CGImageRef (например, вызов bitmapData), растровое изображение не будетпереупаковывать данные как CGImageRef до тех пор, пока они не будут нарисованы или пока не потребуется CGImage по какой-либо другой причине.Так что, безусловно, доступ к данным - это не конец света, и это правильная вещь в некоторых обстоятельствах, но в целом вы должны думать о рисовании.Если вы считаете, что хотите работать с пикселями, обратите внимание на CoreImage - это API в нашей системе, который действительно предназначен для обработки пикселей.

Это совпадает с безопасностью.Проблема, которую мы видели с нашими изменениями в SnowLeopard, заключается в том, что приложения довольно любят жестко кодировать растровые форматы.NSBitmapImageRep может иметь размер 8, 32 или 128 бит на пиксель, он может быть плавающей точкой или нет, может быть предварительно умножен или нет, он может иметь или не иметь альфа-канал и т. Д. Эти аспекты определяются с помощью свойств растрового изображения, например-bitmapFormat.К сожалению, если кто-то хочет извлечь bitmapData из экземпляра NSBitmapImageRep, он обычно просто вызывает bitmapData, обрабатывает данные как (скажем) с предварительно умноженным 32-битным RGBA на пиксель и, если кажется, что работает, называет его днем.

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

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

Это выглядит так:

NSBItmapImageRep *bitmapIGotFromAPIThatDidNotSpecifyFormat;
NSBitmapImageRep *bitmapWhoseFormatIKnow = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:width pixelsHigh:height
                                                  bitsPerSample:bps samplesPerPixel:spp hasAlpha:alpha isPlanar:isPlanar
                                                  colorSpaceName:colorSpaceName bitmapFormat:bitmapFormat bytesPerRow:rowBytes
                                                  bitsPerPixel:pixelBits];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapWhoseFormatIKnow]];
[bitmapIGotFromAPIThatDidNotSpecifyFormat draw];
[NSGraphicsContext restoreGraphicsState];
unsigned char *bitmapDataIUnderstand = [bitmapWhoseFormatIKnow bitmapData];

Это не производит больше копийданные, чем просто доступ к bitmapData объекта bitmapIGotFromAPIThatDidNotSpecifyFormat, поскольку эти данные в любом случае необходимо будет скопировать из резервного CGImage.Также обратите внимание, что это не зависит от того, является ли исходный чертеж растровым.Это способ получить пиксели в известном формате для любого рисунка или просто получить растровое изображение.Это гораздо лучший способ получить растровое изображение, чем, например, вызывать -TIFFRepresentation.Это также лучше, чем блокировка фокуса на NSImage и использование - [NSBitmapImageRep initWithFocusedViewRect:].

Итак, чтобы подвести итог: (1) Рисование выполняется быстро.Играть с пикселями нет.(2) Если вы считаете, что вам нужно поиграть с пикселями, (а) подумайте, есть ли способ сделать это с помощью рисования, или (б) загляните в CoreImage.(3) Если вы все еще хотите получить пиксели, нарисуйте растровое изображение, формат которого вам известен, и посмотрите на эти пиксели.

На самом деле, лучше начать с предыдущего раздела спохожий заголовок - «Сопоставление импеданса NSImage, CGImage и CoreGraphics» - и прочитайте следующий раздел.

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

...