Объекты Bitmap часто являются самыми крупными отдельными участниками объема потребляемой памяти приложения. Будь то значки приложений, изображения уведомлений или медиаконтент, неэффективная обработка растровых изображений может быстро привести к ошибкам нехватки памяти (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 (1920 x 1080) в формате ARGB_8888 занимает 1920 × 1080 × 4 байта ≈ 8,3 МБ.
Битовые карты кучи и общие битовые карты
Битовые карты кучи (нативная куча)
В современных версиях Android (8.0 и выше) пиксельные данные битовых карт хранятся в нативной куче, а в куче Java находится только небольшой объект-оболочка.
Когда приложению нужно показать изображение, оно обычно декодирует его из сжатого графического файла в растровое изображение и сохраняет в куче.
Общие растровые изображения (ashmem/memfd)
Когда растровое изображение передается между процессами (например, через Binder в SystemUI для уведомления), Android не копирует данные пикселей, а использует общую память (ashmem или memfd).
Экземпляр Bitmap можно скопировать в общую память явным образом, вызвав Bitmap.asShared(), или неявным образом, если Bitmap помещается в Parcel (обычно путем добавления Bitmap в Parcelable, например Bundle) и отправляется через Binder IPC.
Когда общий объект Bitmap отправляется через Binder IPC, сами данные пикселей не копируются. Вместо этого в процесс получателя дублируется дескриптор файла, ссылающийся на общую область памяти. Базовая область памяти может использоваться несколькими процессами и не освобождается, пока не будут закрыты все дескрипторы файлов, ссылающиеся на нее.
Изменяемые и неизменяемые битовые карты
- Изменяемые битовые карты можно модифицировать после создания, например с помощью
Canvas. Для них всегда требуется выделение собственной памяти. Если копируется изменяемый объект Bitmap, необходимо создать его полную копию (вторую копию всех данных о пикселях). - Неизменяемые растровые изображения. Нельзя изменить. Это позволяет оптимизировать работу, например использовать один и тот же базовый буфер памяти для разных экземпляров
Bitmap. Растровые изображения, загруженные из ресурсов APK (BitmapFactory), обычно являются неизменяемыми.
Эффективная обработка битовых карт
Объединение и повторное использование битовых карт
Частое выделение и освобождение памяти для битовых карт приводит к перегрузке памяти, из-за чего сборщик мусора работает постоянно. Популярные библиотеки загрузки изображений используют пул растровых изображений.
Для приложений на Java мы рекомендуем использовать Glide, а для приложений на Kotlin – Coil (особенно если вы используете Jetpack Compose).
Когда битовая карта больше не нужна, вместо того чтобы позволить сборщику мусора удалить ее, приложение вызывает метод bitmap.recycle() или возвращает ее в пул. Когда в следующий раз потребуется битовая карта с такими же размерами и конфигурацией, пул предоставит существующий буфер, избегая нового выделения.
Аппаратные растровые изображения
Bitmap.Config.HARDWARE позволяет хранить данные пикселей непосредственно в графической памяти (DMABuf).
- Плюсы:
- Экономия памяти. Не используется куча приложения или нативная куча. Используется память графического процессора. Часто растровые изображения, показанные в интерфейсе приложения, все равно нужно копировать в память графического процессора, поэтому это позволяет избежать операции копирования и дополнительных затрат памяти.
- Эффективность. Отрисовка выполняется очень быстро, поскольку данные уже находятся в графическом процессоре.
- Минусы:
- Неизменяемость. Аппаратные растровые изображения нельзя изменить.
- Медленное считывание. Доступ к пикселям из ЦП (например,
getPixel()) требует больших ресурсов. - Атрибуция. Ее сложнее отслеживать в стандартных инструментах, таких как AHAT (см. ниже).
Распространенные ошибки при использовании памяти битовыми картами
Даже если вы используете современные конфигурации битовых карт, некоторые повторяющиеся шаблоны декодирования и планирования могут вызывать большие скачки использования памяти.
Декодирование растровых изображений с завышенным масштабом
Полноразмерная фотография 4000 × 3000 пикселей занимает 48 МБ в ARGB_8888. Если декодировать изображение полностью, а затем отобразить его в виде значка размером 200 x 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 или AHAT, где каждый экземпляр Bitmap показывает поверхностный размер Java около 56 байт наряду с его многомегабайтным нативным размером, а также в dumpsys meminfo в разделе Native Allocations (Bitmap (malloced)).
Конвейеры с высокой частотой, такие как анализ кадров камеры, оптическое распознавание символов или циклы вывода машинного обучения, часто выделяют новую битовую карту для каждого кадра, вызывая 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 (Выделение памяти в нативном коде). Они обеспечивают гораздо лучшую атрибуцию для растровых изображений, чем общий сводный отчет о приложении:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- Битовая карта (выделенная с помощью malloc). Битовые карты, выделенные в нативной куче процесса. Здесь хранятся стандартные растровые изображения в Android 8.0 и более поздних версиях.
- Битовые карты (не malloced). Битовые карты, которые используют специализированную память, например аппаратные битовые карты или общие битовые карты (через
ashmemилиmemfd).
Если вы выделите общую битовую карту в 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, когда для определенного приложения включена категория atrace gfx.
Начните трассировку. Вам нужно включить категорию
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 несколько раз нажмите кнопки Allocate (Выделить) и Clear (Очистить).
Также нажмите Parcel/Unparcel Bitmap (Упаковать/распаковать растровое изображение).
Проанализируйте трассировку на сайте ui.perfetto.dev.
В разделе процесса com.android.bitmaplab вы увидите:
* Количество растровых изображений. Счетчик, показывающий количество активных растровых изображений.
* Память для растровых изображений. Счетчик, показывающий, сколько байтов памяти используется для растровых изображений.
Высокоуровневые срезы (Perfetto SDK)
BitmapLab также использует Perfetto SDK для создания высокоуровневых срезов для операций с растровыми изображениями. Чтобы найти нужные данные, выполните поиск по запросу BitmapLab_. Вы увидите:
* BitmapLab_parcelUnparcel – фрагменты, охватывающие логику упаковки и распаковки.
* BitmapLab_postNotification – фрагменты, охватывающие процесс публикации уведомлений.
Как отслеживать уведомления
Когда вы нажимаете Post Notification (Опубликовать уведомление), приложение создает уведомление с текущим растровым изображением и отправляет его в систему. Код фреймворка, отвечающий за это, генерирует срезы Perfetto с событиями потока, связывающими упаковку (запись растрового изображения в Parcel для отправки через Binder IPC) и распаковку (чтение растрового изображения из Parcel на принимающей стороне).
На скриншоте ниже показано, как приложение упаковывает большую битовую карту для использования в транзакции Binder, чтобы опубликовать уведомление, и как происходит распаковка в процессе system_server.

С помощью Perfetto вы даже можете отслеживать ту же битовую карту уведомления, когда она распространяется по потокам и процессам, например из потока связывателя в system_server (который реализует сервер связывателя INotificationManager) в рабочие потоки system_server, которые затем могут перенаправить ту же битовую карту в com.android.systemui для отображения на панели уведомлений.
Проблемы с системными приложениями
Системные приложения, такие как SystemUI (уведомления) и 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 | ↑ Наверх | Нативные объявления →