Site icon AppTractor

Приоритизация эффективности использования памяти: важные шаги для Android 17

Google анонсирует важные изменения в работе с памятью в Android 17:

Хотя производительность приложения часто ассоциируют с плавным интерфейсом и быстрым стартом, именно память служит «тихим фундаментом», на котором строятся все эти видимые метрики. Не секрет, что мы наблюдаем сдвиг: объём и использование памяти устройства становятся важнее, чем когда-либо. В Android 17 мы не только продвинулись в оптимизациях памяти на уровне системы, но и предоставляем инструменты и API, которые помогут вам заранее подготовиться к более строгим требованиям к памяти позднее в этом году.

Чтобы обеспечить стабильность устройства, начиная с Android 17 система начнёт принудительно применять лимиты памяти для приложений с учётом общего объёма RAM устройства. Если приложение превысит эти лимиты, Android завершит процесс без какого-либо stack trace.

Помимо таких принудительных завершений, неоптимизированное использование памяти неизбежно ухудшает пользовательский опыт. Когда приложение приближается к лимитам heap-памяти, запускается более частая сборка мусора, что приводит к заметным фризам и подёргиваниям UI. Кроме того, когда на устройстве заканчивается доступная память, система начинает срочно освобождать страницы памяти, что создаёт нагрузку на CPU, увеличивает задержки интерфейса и расход батареи. Если нехватка памяти становится слишком серьёзной, это может вызвать события Low Memory Killer, LMK, которые внезапно завершают фоновые процессы, вынуждая приложения дольше запускаться «с нуля» и терять пользовательское состояние.

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

Краткая версия этой статьи также доступна в видеоформате — посмотрите её.

Понимание лимитов памяти приложений в Android 17

Лимиты памяти для приложений вводятся в Android 17, чтобы не позволить одному «плохому актору» разрушить многозадачность и стабильность всего пользовательского устройства.

Ниже приведены основные причины такого архитектурного изменения.

Чтобы определить, была ли сессия вашего приложения затронута этими ограничениями «в поле», можно вызвать getDescription() у ApplicationExitInfo. Если система применила лимит, причина завершения будет указана как REASON_OTHER, а строка description будет содержать "MemoryLimiter:AnonSwap".

Также можно использовать trigger-based profiling с TRIGGER_TYPE_ANOMALY, чтобы автоматически собирать heap dump при достижении лимита памяти. Кроме того, Android активно работает над тем, чтобы показывать разработчикам больше in-field метрик памяти в Google Play Console.

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

Максимизируйте оптимизацию байткода с помощью R8

Один из самых эффективных способов уменьшить memory footprint приложения — включить оптимизатор R8. За счёт сокращения классов, методов и полей до более коротких имён, а также удаления неиспользуемого кода и ресурсов R8 значительно снижает потребление памяти приложением, уменьшая объём resident code, необходимого во время выполнения.

R8 минимизирует хранящийся в памяти код, снижает отпечаток в памяти и уменьшает риск завершения приложения через LMK. В результате чаще происходят теплые старты вместо медленных холодных. Кроме того, оптимизированный байткод снижает нагрузку на CPU в главном потоке, напрямую уменьшая частоту ANR и делая пользовательский опыт более плавным.

Например, цифровой банк Monzo включил полную оптимизацию R8 и получил снижение ANR на 35%, улучшение холодного старта на 30% и уменьшение общего размера приложения на 9%.

Чтобы правильно настроить R8 в build.gradle:

isShrinkResources = true
isMinifyEnabled = true

Также:

Чтобы получить максимальную оптимизацию, следуйте этим практикам в файле keep rules:

Больше рекомендаций можно найти в документации по keep rules.

Лучшие практики R8 для разработчиков библиотек

Если вы разрабатываете библиотеку, строго помещайте правила, которые нужны вашим потребителям, в consumer rules-файл, а внутренние правила защиты самой библиотеки — в proguard-rules.pro.

Подробнее об оптимизации библиотек можно узнать в материалах про оптимизацию библиотек

R8 Configuration Analyzer

Чтобы провести аудит оптимизации R8, используйте Configuration Analyzer. Он показывает текущее состояние оптимизации с оценками Obfuscation, Optimization и Shrinking.

С помощью Configuration Analyzer можно понять, сколько классов, методов или полей исключены из оптимизации каждым keep rule. Уточняйте слишком широкие package-wide правила, чтобы разблокировать максимальную оптимизацию.

Также Configuration Analyzer помогает находить keep rules, которые перекрывают другие правила, избыточные keep rules и неиспользуемые keep rules.

R8 Agent Skill

Также можно использовать R8 Agent Skill с Android Studio agent или другими AI-инструментами, чтобы находить неправильные конфигурации и уточнять правила. Это может улучшить производительность приложения.

При этом выводы AI-инструментов требуют технической проверки.

Оптимизируйте загрузку изображений

Обычно bitmaps — это самые крупные распространённые объекты, которые находятся в памяти приложения. Они представляют собой финальную стадию процесса загрузки изображения: сжатые файлы, например JPEG или PNG, декодируются в raw pixel data для отображения.

Это означает, что маленькое сжатое изображение размером 100 КБ может превратиться в несколько мегабайт RAM, потому что потребление памяти определяется pixel dimensions и color depth. Поскольку операции с bitmap часто находятся на критическом пути в отрисовке кадров, неоптимизированные изображения приводят к серьёзному раздуванию памяти и задержкам в UI.

Google рекомендует использовать библиотеки загрузки изображений: Coil для Kotlin-first проектов, особенно при разработке с Jetpack Compose, и Glide для Java-based приложений.

Пять лучших практик

Подробнее можно посмотреть в документации по оптимизации производительности изображений.

Инструменты Android Studio

Также можно устранять дублирующиеся bitmaps с помощью Android Studio Narwhal 4. Вот как найти их за пять шагов:

  1. Откройте вкладку Profiler в Android Studio.
  2. Нажмите Heap Dump или Analyze Memory Usage и запустите запись, чтобы сделать снепшот текущего состояния памяти приложения.
  3. Просмотрите результаты анализа и найдите жёлтый предупреждающий треугольник ⚠️, которым Android Studio помечает дублированные битмапы, хранящиеся в памяти несколько раз. Либо в header профайлера выберите Filter by и укажите Duplicate Bitmaps.
  4. Нажмите на любой помеченный элемент, чтобы открыть Bitmap Preview и увидеть, какое именно изображение дублируется.
  5. Используйте это визуальное подтверждение, чтобы найти в коде избыточную логику загрузки и реализовать более корректную стратегию кэширования.

Ищите жёлтый предупреждающий треугольник ⚠️ в heap dumps при использовании Android Studio Profiler.

Находите и исправляйте утечки памяти с помощью Android Studio

Утечки памяти в Android возникают, когда код удерживает ссылку на объект намного дольше, чем длится его жизненный цикл. Это не даёт Garbage Collector освободить память и со временем приводит к деградации производительности или OutOfMemoryError.

Android Studio Panda 3 включает специальную profiler-задачу LeakCanary, позволяющую разработчикам анализировать утечки памяти в реальном времени и сопоставлять leak traces прямо внутри IDE.

Борьба с утечками памяти: от задачи до победы

Profiler-задача LeakCanary в Android Studio переносит анализ утечек памяти с устройства на машину разработчика. В результате анализ выполняется значительно быстрее по сравнению с on-device leak analysis.

Кроме того, анализ утечек теперь контекстуализирован внутри IDE и полностью интегрирован с исходным кодом. Это даёт возможности вроде Go to declaration и другие полезные связи с кодом, что сильно уменьшает трение и время, необходимое на расследование и исправление утечек.

Примеры распространённых утечек памяти

Утечки памяти возникают, когда объект остаётся в памяти дольше предполагаемого срока жизни. Обычно это происходит из-за:

Освобождайте память, когда приложение выходит из видимого состояния

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

Обычно Android освобождает память приложения, когда оно не видно пользователю: например, отбрасывает часть кода и data pages из памяти или сжимает выделенную кучу. Когда пользователь возвращается в приложение и оно пытается обратиться к памяти, которая была освобождена, ОС подгружает её обратно по запросу. Такой свап может быть медленным и вызывать неожиданные заминки и задержки.

Если оставить ОС самой решать, какую память освободить, может оказаться, что она освободила именно ту память, которая понадобится приложению сразу после возобновления работы. Вместо этого приложение может добровольно сбрасывать выделенную память, которую можно позже недорого восстановить on demand.

Для этого можно реализовать интерфейс ComponentCallbacks2. Его можно использовать в Activity, Fragment, Service или даже в вашем кастомном классе Application. Использование в Application особенно эффективно для глобального управления кэшами.

Callback onTrimMemory() уведомляет приложение о lifecycle- или memory-related событиях, которые являются хорошим моментом для добровольного сокращения потребления памяти.

С точки зрения memory lifecycle management, реализация должна фокусироваться исключительно на TRIM_MEMORY_UI_HIDDEN и TRIM_MEMORY_BACKGROUND. Начиная с Android 14, система перестала отправлять уведомления для других legacy-констант, которые были официально deprecated в Android 15.

TRIM_MEMORY_UI_HIDDEN означает, что UI приложения больше не виден пользователю. Это возможность освободить значительные memory allocations, строго связанные с интерфейсом: bitmaps, буферы видеоплеера, сложные animation resources и так далее.

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

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Примечание: интеграция onTrimMemory может зависеть от поддержки SDK. Например, некоторые игры полагаются на игровой движок, который должен включить такую возможность. Проверьте документацию по оптимизации памяти для игр.

Расширенное наблюдение за памятью через ProfilingManager

Чтобы ловить и диагностировать memory issues «в поле», которые не удаётся воспроизвести локально, используйте API ProfilingManager. Этот продвинутый API наблюдения, появившийся в Android 15, позволяет программно собирать real-user Perfetto profiles.

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

Android 17 добавляет новые event-driven триггеры, в первую очередь TRIGGER_TYPE_OOM и TRIGGER_TYPE_ANOMALY.

OOM trigger автоматически собирает Java heap dump в момент падения из-за OutOfMemoryError, предоставляя точное состояние аллокаций. Собранный OOM profile становится доступен при следующем запуске приложения после регистрации колбека registerForAllProfilingResults.

Anomaly trigger обнаруживает серьёзные проблемы производительности, например excessive binder spam или превышение memory thresholds. Memory anomaly передаёт heap dump непосредственно перед тем, как система завершит приложение.

  val profilingManager = 
applicationContext.getSystemService(ProfilingManager::class.java)
    val triggers = ArrayList()  


    triggers.add(ProfilingTrigger.Builder(
                 ProfilingTrigger.TRIGGER_TYPE_ANOMALY))
    val mainExecutor: Executor = Executors.newSingleThreadExecutor()
    val resultCallback = Consumer { profilingResult ->
        if (profilingResult.errorCode != ProfilingResult.ERROR_NONE) {
            // upload profile result to server for further analysis          
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } 

    profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

После того как heap dump собран, его можно скачать с сервера или локально через adb pull, а затем перетащить файл в Perfetto UI.

Чтобы упростить процесс отладки памяти, используйте Heap Dump Explorer — это новый default view для heap dumps в Perfetto UI. Инструмент предоставляет удобный интерфейс для инспекции Java heap dumps: позволяет визуализировать иерархии аллокаций объектов, вычислять retained memory size и находить кратчайший путь от GC root.

С помощью Heap Dump Explorer можно быстро находить утечки памяти, раздутые retained объекты, например чрезмерные потребления памяти на изображения, и анализировать выделение памяти в куче на объекты в одном месте.

Заключение

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

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

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

Exit mobile version