Растровые объекты часто являются крупнейшими отдельными элементами, занимающими больше всего памяти в приложении. Будь то значки приложений, изображения уведомлений или медиаконтент, неэффективная обработка растровых изображений может быстро привести к ошибкам нехватки памяти (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 обеспечивает превосходную визуализацию растровых изображений.
- В BitmapLab выделите несколько растровых изображений.
Создайте дамп кучи с помощью флага
-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Откройте
localhost:7100и найдите ссылку «Bitmaps» в боковой панели или выполните поиск по классуBitmap.AHAT фактически отображает растровые изображения в браузере, что позволяет легко определить, какие изображения занимают много памяти.

3. Растровые дорожки в Perfetto
Perfetto может отслеживать выделение памяти и количество битовых изображений с течением времени. Эти счетчики генерируются фреймворком Android, когда для конкретного приложения включена категория трассировки графики (gfx atrace).
Запустите трассировку. Необходимо указать категорию
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В BitmapLab многократно нажимайте кнопки «Выделить» и «Очистить» .
Также нажмите «Распечатать/Отправить посылку» .
Проанализируйте трассировку в файле 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, вы можете даже отслеживать распространение одного и того же битового изображения уведомления между потоками и процессами, например, от потока-связывателя в system_server (который реализует сервер связывания INotificationManager ) к рабочим потокам system_server , которые затем могут пересылать то же битовое изображение в com.android.systemui для отображения в панели уведомлений.
Проблемы системных приложений
Системные приложения, такие как SystemUI (уведомления) и Launcher, сталкиваются с уникальными проблемами:
- Неограниченное количество контента : уведомлений и виджетов может быть очень много. Если каждый из них содержит большое растровое изображение, система может быстро исчерпать память.
- Дублирование : один и тот же значок приложения может храниться в кэше Launcher, в области уведомлений SystemUI и в приложении «Настройки».
- Совместное использование через аппаратные буферы : Для решения этой проблемы компоненты системы переходят к централизованной службе «выгрузки образов», которая обеспечивает совместное использование экземпляров
HardwareBufferмежду процессами. Атрибуция 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 | ↑ Вверх | Нативный →