Мы уже рассказывали об уменьшении размера APK в предыдущей статье. В этой мы подробно рассмотрим оптимизацию памяти.
Почему память приложений важна?
Эффективные приложения, использующие минимум памяти, лучше работают, экономят ресурсы устройства и продлевают время автономной работы. Они обеспечивают плавную работу пользователя и их больше выбирают в магазинах приложений. Такие приложения совместимы с широким спектром устройств.
Методы трассировки памяти
Для отслеживания памяти приложения можно воспользоваться одним из следующих подходов:
1. Команда ADB
Чтобы получить данные о памяти приложения в конкретном инстансе, выполните в терминале следующую команду:
adb shell dumpsys meminfo appPackageName
Примечание: Замените appPackageName
на фактическое имя пакета приложения, которое вы хотите отслеживать.
На приведенном выше скриншоте видно, что приложение использует 42 МБ памяти.
Преимущества:
- Получение данных о памяти любого приложения на устройстве.
Ограничения:
- Невозможно отслеживать изменения памяти в виде графиков, основанных на взаимодействии с пользователем, как это делает Android Profiler.
2. Android Profiler
Шаги для запуска профилировщика: View (на верхней панели) -> Tool Windows -> Profiler -> Нажмите «+» -> Выберите устройство и пакет.
Преимущества:
- Профилировщик Android позволяет отслеживать поведение во время выполнения и потребление памяти в зависимости от использования приложения.
- Он позволяет получить полное представление о памяти приложения.
Ограничения:
- Можно профилировать только «отлаживаемые приложения».
Примечание: Обратите внимание, что в этом блоге мы не будем рассматривать анализ памяти. Для получения более подробной информации обратитесь к официальной документации, доступной здесь.
Наше приложение в значительной степени ориентировано на работу с изображениями, и мы заметили, что после прокрутки 47 изображений объем памяти приложения превысил 500 МБ (как показано на скриншоте выше). Это повышает риск возникновения Out-of-Memory Exceptions (OOM). Мы осознали необходимость оптимизации памяти для улучшения пользовательского опыта и предприняли следующие шаги.
1. Изменение цветового формата пикселей
Пиксельный цветовой формат (Pixel Color Format), также известный как формат пикселя, определяет способ хранения в памяти цветовой информации для каждого пикселя изображения. Он определяет расположение красной, зеленой, синей и альфа (прозрачности) компонент, что влияет на качество цветопередачи и производительность рендеринга.
В Android поддерживается несколько цветовых форматов. Однако ниже мы остановимся на наиболее часто используемых.
- ARGB_8888 (32 бита на пиксель) — 8 бит для альфы (прозрачности), 8 бит для красного, 8 бит для зеленого и 8 бит для синего цветов
- RGB_565 (16 бит на пиксель) — 5 бит для красного, 6 бит для зеленого и 5 бит для синего, альфа отсутствует.
Как видно из приведенного изображения, разница между RGB 565 и ARGB 8888 едва заметна, но при использовании RGB 565 память сокращается примерно на 50%.
Для рендеринга изображений мы используем библиотеку Glide. По умолчанию Glide использует для загрузки изображений цветовой формат ARGB 8888. Однако Glide предоставляет возможность гибко настраивать предпочтительный формат цвета пикселей.
@GlideModule class CustomGlideModule : AppGlideModule() { override fun applyOptions(context: Context, builder: GlideBuilder) { builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565)) } }
Как показано выше, мы изменили формат цвета по умолчанию на RGB 565 на уровне приложения. Такое изменение конфигурации приводит к снижению потребления памяти на пиксель на 50%. Для проектов, в которых большое место занимают изображения, это решение имеет большое значение.
Примечание: RGB 565 имеет определенные ограничения, такие как незначительные цветовые вариации и отсутствие поддержки прозрачности. Это может привести к снижению точности цветопередачи для конкретных изображений, поэтому выбор следует делать исходя из конкретных требований приложения.
2. Изменение стратегии Glide DiskCacheStrategy
Стратегия DiskCacheStrategy в первую очередь определяет, каким образом изображения будут кэшироваться на устройстве.
Ранее мы использовали DiskCacheStrategy.All. Однако, проведя некоторые исследования, мы поняли, что DiskCacheStrategy.Resource больше соответствует нашим специфическим потребностям.
- DiskCacheStrategy.ALL — кэширование всех версий изображения.
- DiskCacheStrategy.Resource — кэширует на диске только конечное изображение после всех преобразований (например, изменений размера, кадрирования). Эта стратегия идеально подходит для случаев, когда необходимо кэшировать не исходные данные, а полностью обработанное изображение.
Дополнительные стратегии DiskCacheStrategies можно найти в официальной документации.
Например, в таких приложениях, как WhatsApp, изображения часто отображаются в сжатом формате. Но иногда пользователи могут захотеть поделиться этими изображениями в исходном виде с высоким разрешением. В таких случаях DiskCacheStrategy.Resource не подойдет.
Таким образом, выбор стратегии DiskCacheStrategy зависит от конкретных требований вашего приложения.
3. Модификация значения OffscreenPageLimit
Первоначально, чтобы минимизировать задержки и предотвратить появление пустых экранов, мы установили для ViewPager значение OffscreenPageLimit равным 3. Мы предполагали, что ViewPager будет кэшировать предыдущую, текущую и следующую страницы. Однако при дальнейшем исследовании мы обнаружили, что на самом деле он кэширует три предыдущие, текущую и следующую страницы, в результате чего в памяти хранится в общей сложности 7 изображений высокого разрешения большого размера.
На основании проведенного анализа мы решили уменьшить значение параметра OffscreenPageLimit до 1. Это не только уменьшило потребление памяти, но и обеспечило бесперебойную работу приложения без каких-либо проблем с задержками.
viewPager.offscreenPageLimit = 1
4. Очистка кэша в onViewRecycled
onViewRecycled
— это колбек метод в RecyclerView.Adapter, который срабатывает при переработке представления. Он обычно используется в Glide для отмены загрузки изображений, что позволяет оптимизировать использование памяти и сети.
override fun onViewRecycled(holder: ChildBingeHolder) { GlideApp.with(context).clear(yourView) }
Очистка кэша View при переработке в адаптере позволяет освободить память.
5. Указание размера изображения
У нас был ImageView размером 64×64 px (маленький), но наш API предоставлял изображение размером 512×512 px (большое). Декодирование такого большого изображения для маленького заполнителя неэффективно с точки зрения использования памяти и производительности приложения.
Для решения этой проблемы мы указали ширину и высоту представления, чтобы обеспечить корректное масштабирование изображения, а не загружать в память слишком большое изображение.
Glide.with(this) .load(IMAGE_URL) .override(targetWidth, targetHeight) .into(imageView)
Использование функции override()
позволяет значительно сократить потребление памяти за счет указания нужных размеров изображения, что особенно удобно при работе с большими изображениями или несколькими изображениями, отображаемыми одновременно.
6. Обработка onTrimMemory
Реализация onTrimMemory(int)
позволяет постепенно освобождать память в зависимости от системных ограничений. Это повышает отзывчивость системы и удобство работы пользователя, позволяя процессу работать дольше. Без обрезания ресурсов система может убить ваш кэшированный процесс, что потребует от вашего приложения перезапуска и восстановления состояния при возвращении пользователя.
Примечание: Приведенная ниже обработка уровней памяти соответствует специфическим требованиям нашего приложения. Мы настраиваем уровни памяти по мере необходимости. Документация по уровням памяти.
override fun onTrimMemory(level: Int) { //Memory Levels documentation : https://developer.android.com/reference/android/content/ComponentCallbacks2 // TRIM_MEMORY_COMPLETE & TRIM_MEMORY_MODERATE are the levels which are called when the app is in background if (level == android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { GlideApp.get(this).clearMemory() } else if (level == android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) { GlideApp.with(this).onTrimMemory(TRIM_MEMORY_MODERATE) } }
Результат
Выполнив указанные шаги и внеся некоторые незначительные коррективы, мы успешно добились значительных улучшений в потреблении приложением памяти.
Использование памяти уменьшилось с 515 МБ при прокрутке 47 изображений до 137 МБ (более чем на 70%) при прокрутке 67 изображений, как показано в таблице. Это улучшение позволяет нам добавлять в приложение больше изображений, не опасаясь нехватки памяти.
Заключение
Таким образом, наша оптимизация позволила значительно уменьшить размер APK и оптимизировать потребление памяти. Эти усилия не только улучшили пользовательский опыт, но и проложили путь к более эффективной работе продукта, позволив нам предоставлять клиентам более качественное, быстрое и оптимизированное приложение.