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

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

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

Для изучения этих концепций мы воспользуемся примером приложения 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 | ↑ Вверх | Нативный →