Растровые изображения и память

Растровые объекты часто являются крупнейшими отдельными элементами, занимающими больше всего памяти в приложении. Будь то значки приложений, изображения уведомлений или медиаконтент, неэффективная обработка растровых изображений может быстро привести к ошибкам нехватки памяти (OOM) и нехватке памяти в масштабах всей системы.

Настройки растрового изображения и данные пикселей.

Объем памяти, потребляемый растровым изображением, в основном определяется его размерами (ширина × высота) и конфигурацией ( Bitmap.Config ).

В конфигурации определяется, сколько байтов используется для представления каждого пикселя:

Конфигурация Байты на пиксель Описание
ALPHA_8 1 Только альфа-канал (прозрачность). Полезен для создания масок.
RGB_565 2 Красный (5 бит), зеленый (6 бит), синий (5 бит). Без альфа-канала. Подходит для непрозрачных изображений, где высокая точность цветопередачи не критична.
ARGB_8888 4 Альфа-канал, красный, зеленый, синий (по 8 бит каждый). Стандартные и наиболее распространенные значения.
RGBA_F16 8 Числа с плавающей запятой половинной точности. Используются для контента с широким цветовым охватом и HDR.
HARDWARE Н/Д Хранится в графической памяти (gralloc/DMABuf). См. раздел «Аппаратные растровые изображения» .

Формула расчета объема памяти: Memory (Bytes) = Width × Height × Bytes Per Pixel

Например, полноэкранное изображение на устройстве с разрешением 1080p (1920x1080) в ARGB_8888 занимает: 1920 × 1080 × 4 байта ≈ 8,3 МБ.

Битовые карты кучи против битовых карт общей памяти

Битовые карты кучи (собственная куча)

В современных версиях Android (8.0 и выше) данные пикселей растрового изображения хранятся в нативной куче , тогда как в куче Java находится лишь небольшой объект-обертка.

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

Общие битовые карты (ashmem/memfd)

При передаче растрового изображения между процессами (например, через Binder в SystemUI для уведомления) Android избегает копирования данных пикселей, используя общую память ( ashmem или memfd ).

Экземпляр Bitmap можно скопировать в разделяемую память явным образом, вызвав метод Bitmap.asShared() , или неявно, если Bitmap помещен внутрь Parcel (обычно путем добавления Bitmap в Parcelable , например, Bundle ) и отправлен по межпроцессному взаимодействию Binder.

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

Изменяемые и неизменяемые битовые карты

  • Изменяемые растровые изображения : могут быть изменены после создания (например, с помощью Canvas ). Для них всегда требуется собственное выделенное пространство памяти. При копировании изменяемого растрового изображения необходимо создать глубокую копию (вторую копию всех пиксельных данных).
  • Неизменяемые растровые изображения : их нельзя изменить. Это позволяет оптимизировать процессы, например, использовать один и тот же базовый буфер памяти для разных экземпляров Bitmap . Растровые изображения, загружаемые из ресурсов APK ( BitmapFactory ), обычно являются неизменяемыми.

Эффективная обработка растровых изображений

Объединение и повторное использование битовых изображений

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

Google рекомендует Glide в качестве решения для приложений на Java, а Coil — для приложений на Kotlin (особенно при использовании Jetpack Compose).

Когда растровое изображение больше не требуется, вместо того чтобы позволить сборщику мусора удалить его, приложение вызывает bitmap.recycle() или возвращает его в пул. В следующий раз, когда потребуется растровое изображение тех же размеров и конфигурации, пул предоставит существующий буфер, избегая нового выделения памяти.

Аппаратные растровые изображения

Bitmap.Config.HARDWARE позволяет хранить данные пикселей непосредственно в графической памяти (DMABuf).

  • Плюсы :
    • Экономия памяти : не использует память приложения или собственную кучу памяти; использует память графического процессора. Часто растровые изображения, отображаемые в пользовательском интерфейсе приложения, в любом случае необходимо копировать в память графического процессора, поэтому это позволяет избежать операции копирования и дополнительных затрат памяти.
    • Производительность : Отрисовка происходит чрезвычайно быстро, поскольку данные уже находятся на графическом процессоре.
  • Минусы :
    • Неизменяемость : аппаратные растровые изображения не могут быть изменены.
    • Операции чтения выполняются медленно : доступ к пикселям из ЦП (например, с помощью getPixel() ) очень затратен.
    • Атрибуцию : Отслеживать её сложнее с помощью стандартных инструментов, таких как AHAT (см. ниже).

Распространенные ошибки при работе с битовыми картами и памятью

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

Декодирование увеличенных растровых изображений

Фотография с полным разрешением 4000 × 3000 пикселей занимает 48 МБ в цветовом пространстве ARGB_8888 . Декодирование этого изображения целиком для последующего отображения в миниатюре размером 200 × 150 пикселей приводит к потере более 99% выделенного буфера пикселей.

При декодировании изображений напрямую с помощью ImageDecoder или BitmapFactory , во время прохода декодирования происходит уменьшение разрешения до размеров целевого представления с помощью ImageDecoder.setTargetSize() или BitmapFactory.Options.inSampleSize . Библиотеки загрузки изображений, такие как Glide и Coil, выполняют это уменьшение разрешения автоматически, если вы указываете ограниченный размер целевого представления.

Например, при декодировании растрового изображения с помощью ImageDecoder передайте OnHeaderDecodedListener , который уменьшит размеры выходного изображения до целевого размера представления:

val source = ImageDecoder.createSource(resources, R.drawable.high_res_photo)
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
    if (info.size.width > targetWidth || info.size.height > targetHeight) {
        decoder.setTargetSize(targetWidth, targetHeight)
    }
}

Высокая степень сохранения результатов при параллельном декодировании.

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

Высокая степень параллельного удержания приводит к увеличению пикового объема памяти кучи и может вызвать завершение lmkd до окончания пакета. Ограничьте параллелизм декодирования с помощью ограниченного пула потоков, семафора или диспетчера сопрограмм, например Dispatchers.IO.limitedParallelism(2) , чтобы одновременно декодировалось лишь несколько битовых карт.

Неиспользованные временные растровые изображения в циклах обработки кадров

В Android 8.0 и выше объект-оболочка Java Bitmap занимает всего около 56 байт в куче Java, в то время как его буфер пикселей находится в нативной куче и может занимать несколько мегабайт. Вы можете проверить это разделение в Android Studio Memory Profiler или AHAT , где каждый экземпляр Bitmap показывает размер в Java около 56 байт наряду с его многомегабайтным нативным размером, а также в dumpsys meminfo в разделе Native Allocations ( Bitmap (malloced) ).

В высокочастотных конвейерах обработки данных, таких как анализ кадров с камеры, оптическое распознавание текста (OCR) или циклы вывода машинного обучения, часто выделяется новый растровый файл для каждого кадра путем вызова методов ImageProxy.toBitmap() и Bitmap.createBitmap() для поворота или обрезки. Удаление ссылок на устаревшие кадры без их повторного использования может привести к значительному увеличению объема используемой памяти. Небольшие Java-обертки практически не увеличивают загрузку кучи Java, поэтому они не запускают сборщик мусора достаточно быстро, чтобы предотвратить накопление сотен мегабайт собственных пиксельных буферов до того, как NativeAllocationRegistry освободит их.

При обработке кадров в цикле с высокой интенсивностью, по возможности, повторно используйте предварительно выделенные буферы или явно вызывайте метод bitmap.recycle() для промежуточных растровых изображений сразу после завершения обработки каждого кадра.

Практическое упражнение: исследование растровых изображений.

Для изучения этих концепций мы воспользуемся примером приложения BitmapLab .

1. Измерение с помощью dumpsys meminfo

Запустите BitmapLab и выберите ALLOCATE 10MB ARGB_8888 . Затем выполните:

adb shell dumpsys meminfo -s com.android.bitmaplab

В современных версиях Android найдите раздел «Native Allocations» . Он предоставляет гораздо более удобное распределение ресурсов для растровых изображений, чем стандартный раздел «App Summary» :

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • Bitmap (malloced) : растровые изображения, выделенные в собственной куче процесса. Именно здесь находится большинство стандартных растровых изображений в Android 8.0 и выше.
  • Битовые карты (без выделения памяти) : Битовые карты, использующие специализированную память, такую ​​как аппаратные битовые карты или разделяемые битовые карты (через ashmem или memfd ).

Если вы выделите общий битовый объект (Shared Bitmap) в BitmapLab , вы увидите его отображение в Bitmap (nonmalloced) :

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240
Bitmap (nonmalloced):        1                          10240  # <--- Shared Bitmap!

Отслеживание общих растровых изображений

В некоторых версиях Android и конфигурациях ядра dumpsys meminfo также обеспечивает высокоточное отслеживание битовых изображений, отображаемых в адресное пространство процесса через файловые дескрипторы.

По умолчанию для общих растровых изображений используется общее имя ("bitmap"). Чтобы включить подробную информацию об авторстве и уникальное отслеживание растровых изображений (идентификация общих растровых изображений в разных процессах), необходимо включить следующее системное свойство:

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

При включении этой функции области ashmem в /proc/<pid>/smaps будут иметь более описательные имена. meminfo воспользуется этим, и результаты будут выглядеть следующим образом:

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • Отображено : общий размер всех отображений памяти, связанных с битовыми картами.
  • Уникальность : Размер растровых изображений, учитывающий только уникальные значения (т.е. два или более сопоставления одних и тех же базовых общих пиксельных данных растрового изображения учитываются только один раз).

2. Растровые изображения в AHAT

AHAT обеспечивает превосходную визуализацию растровых изображений.

  1. В BitmapLab выделите несколько растровых изображений.
  2. Создайте дамп кучи с помощью флага -b (чтобы включить собственные данные растрового изображения):

    adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof
    adb pull /data/local/tmp/bitmaps.hprof .
    ahat bitmaps.hprof
    
  3. Откройте localhost:7100 и найдите ссылку «Bitmaps» в боковой панели или выполните поиск по классу Bitmap .

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

AHAT отображает отрисованные растровые изображения

3. Растровые дорожки в Perfetto

Perfetto может отслеживать выделение памяти и количество битовых изображений с течением времени. Эти счетчики генерируются фреймворком Android, когда для конкретного приложения включена категория трассировки графики (gfx atrace).

  1. Запустите трассировку. Необходимо указать категорию gfx и выбрать конкретный пакет приложения, используя флаг -a :

    external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \
        -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplab
    
  2. В BitmapLab многократно нажимайте кнопки «Выделить» и «Очистить» .

  3. Также нажмите «Распечатать/Отправить посылку» .

  4. Проанализируйте трассировку в файле ui.perfetto.dev .

В разделе процесса для com.android.bitmaplab вы увидите: * Bitmap Count : счетчик, показывающий количество активных растровых изображений. * Bitmap Memory : счетчик, показывающий общее количество байтов, используемых растровыми изображениями.

Срезы высокого уровня (Perfetto SDK)

BitmapLab также использует SDK Perfetto для генерации высокоуровневых фрагментов для операций с растровыми изображениями. Найдите BitmapLab_ в трассировке, чтобы обнаружить: * BitmapLab_parcelUnparcel : фрагменты, описывающие логику разделения и распаковки пакетов. * BitmapLab_postNotification : фрагменты, описывающие процесс отправки уведомлений.

Отслеживание потоков уведомлений

При нажатии кнопки «Отправить уведомление » приложение создает уведомление, содержащее текущее растровое изображение, и отправляет его в систему. Код фреймворка, отвечающий за это, генерирует фрагменты Perfetto с событиями потока, соединяющими процесс парсинга (запись растрового изображения в объект Parcel для отправки по межпроцессному взаимодействию Binder) и депарсинга (чтение растрового изображения из объекта Parcel на принимающей стороне).

На скриншоте ниже вы можете увидеть, как приложение разделяет большой битовый рисунок на части для использования в транзакции Binder при отправке уведомления, а также соответствующее разъединение в процессе system_server .

Perfetto демонстрирует поток данных от BitmapLab к system_server через уведомление.

Используя Perfetto, вы можете даже отслеживать распространение одного и того же битового изображения уведомления между потоками и процессами, например, от потока-связывателя в system_server (который реализует сервер связывания INotificationManager ) к рабочим потокам system_server , которые затем могут пересылать то же битовое изображение в com.android.systemui для отображения в панели уведомлений.

Проблемы системных приложений

Системные приложения, такие как SystemUI (уведомления) и Launcher, сталкиваются с уникальными проблемами:

  1. Неограниченное количество контента : уведомлений и виджетов может быть очень много. Если каждый из них содержит большое растровое изображение, система может быстро исчерпать память.
  2. Дублирование : один и тот же значок приложения может храниться в кэше Launcher, в области уведомлений SystemUI и в приложении «Настройки».
  3. Совместное использование через аппаратные буферы : Для решения этой проблемы компоненты системы переходят к централизованной службе «выгрузки образов», которая обеспечивает совместное использование экземпляров HardwareBuffer между процессами.
  4. Атрибуция DMABuf : Аппаратные битовые карты экономят место в куче, но используют память DMABuf, которую сложнее отнести к конкретному процессу с помощью стандартных инструментов работы с памятью.

    Используйте adb shell dmabuf_dump чтобы увидеть распределение буферов DMABuf по всей системе. Этот инструмент предоставляет подробную информацию о буферах для каждого процесса:

     droid.bitmaplab:19562
                      Name              Rss              Pss         nr_procs            Inode               Exporter
                 <unknown>          3840 kB          1280 kB                3             3397              virtio_gpu
                    system            12 kB             4 kB                3             3398                  system
                 <unknown>          3840 kB          1920 kB                2             3399              virtio_gpu
                    system            12 kB             6 kB                2             3400                  system
             PROCESS TOTAL         11556 kB          5136 kB
    
    • RSS : Общий размер буфера, если он отображается в процессе.
    • PSS : Пропорциональный размер (RSS, деленный на количество процессов, использующих буфер). Это лучший показатель для учета.
    • nr_procs : Количество процессов, в данный момент владеющих ссылкой на этот буфер.
    • Экспортер : драйвер, выделивший буфер (например, virtio_gpu в Cuttlefish или специфичная для производителя куча Ion/DMA-BUF на аппаратном уровне).

    Также можно использовать adb shell dmabuf_dump -b для получения сводной информации обо всех буферах и общем объеме использования DMA-BUF в масштабах всей системы.


← Java | ↑ Вверх | Нативный →