Управляйте памятью вашего приложения.

На этой странице объясняется, как заблаговременно сократить использование памяти в вашем приложении. Информацию о том, как операционная система Android управляет памятью, см. в разделе «Обзор управления памятью» .

Оперативная память (RAM) — ценный ресурс для любой среды разработки программного обеспечения, и она ещё более ценна для мобильной операционной системы, где физическая память часто ограничена. Хотя и среда выполнения Android (ART), и виртуальная машина Dalvik выполняют стандартную сборку мусора, это не означает, что можно игнорировать, когда и где ваше приложение выделяет и освобождает память. Вам всё ещё необходимо избегать утечек памяти — обычно вызванных сохранением ссылок на объекты в статических переменных-членах — и освобождать любые объекты Reference в соответствующее время, определяемое функциями обратного вызова жизненного цикла.

Сократите объем кода и потребляемых ресурсов вашего приложения.

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

Уменьшите общий размер приложения, включив R8.

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

Вы можете использовать R8 для уменьшения объема памяти, используемой вашим приложением . Хотя R8 традиционно известен уменьшением размера APK-файлов , он оказывает прямое положительное влияние на память во время выполнения (ОЗУ). R8 анализирует байт-код вашего приложения, чтобы удалить неиспользуемый код, объединить избыточные классы, встроить методы и минимизировать идентификаторы. Загружая меньше скомпилированного байт-кода из APK в ОЗУ, он уменьшает общий базовый объем памяти, используемый приложением. Кроме того, минимизация имен классов, методов и полей в более короткие идентификаторы напрямую снижает накладные расходы на ОЗУ. Оптимизации, такие как слияние классов и обширное встраивание методов, также заменяют дорогостоящие операции поиска и выделения памяти во время выполнения, что приводит к оптимизации памяти в куче и стеке.

Поймите правила хранения

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

Неправильно сформулированные правила сохранения (keep rules) мешают R8 оптимизировать значительные части вашего кода. Избегайте слишком общих правил сохранения и следуйте этим рекомендациям:

  • Глобальные правила, которых следует избегать:
    • -dontoptimize : Полностью отключает оптимизацию для всего приложения, что приводит к увеличению размера и замедлению работы исполняемых файлов.
    • -dontshrink : Предотвращает удаление неиспользуемого кода и ресурсов.
    • -dontobfuscate : Предотвращает минификацию имен, что приводит к потере ценной экономии памяти (особенно в больших приложениях).
  • Избегайте использования символов подстановки, применяемых ко всему пакету: общие правила, такие как -keep class com.example.package.** { *; } , заставляют R8 сохранять каждый класс, поле и метод в этом пакете. Это полностью исключает возможность R8 удалять, оптимизировать или минимизировать код в этом пакете.

  • Используйте стандартный конфигурационный файл R8: всегда используйте proguard-android-optimize.txt .

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

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

Будьте осторожны при использовании сторонних библиотек.

Код внешних библиотек часто не предназначен для мобильных устройств и может быть неэффективным при работе с мобильными клиентами. При использовании внешней библиотеки может потребоваться её оптимизация для мобильных устройств. Заранее спланируйте эту работу и проанализируйте библиотеку с точки зрения размера кода и объёма используемой оперативной памяти, прежде чем использовать её.

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

Хотя оптимизация вашего приложения с помощью R8 может удалить неиспользуемый код из зависимостей, её эффективность часто ограничена внутренней конфигурацией библиотеки. Например, общие правила сохранения или использование рефлексии внутри библиотеки могут помешать R8 уменьшить размер кода, что приведет к увеличению потребления памяти. Для получения рекомендаций по выбору эффективных библиотек см. раздел «Выбирайте библиотеки с умом» .

Избегайте использования разделяемых библиотек только для одной или двух функций из десятков. Не добавляйте большой объем кода и накладных расходов, которые вам не нужны. Принимая решение об использовании библиотеки, ищите реализацию, которая максимально соответствует вашим потребностям. В противном случае, вы можете решить создать собственную реализацию.

Для внедрения зависимостей используйте Hilt или Dagger 2.

Фреймворки внедрения зависимостей могут упростить написание кода и предоставить адаптивную среду, полезную для тестирования и других изменений конфигурации.

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

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

При использовании внедрения зависимостей следует избегать утечек памяти, обеспечивая правильную область видимости объектов. Хранение объектов дольше, чем необходимо, путем привязки их к неправильному жизненному циклу может привести к утечкам памяти. Для получения дополнительной информации см. руководство по предотвращению утечек памяти при работе с объектами с заданной областью видимости .

Подходите к загрузке изображений осознанно.

Графические растровые изображения обычно являются самыми большими распространенными объектами, хранящимися в памяти вашего приложения. Даже если вы работаете со сжатыми файлами, такими как JPEG, файл необходимо преобразовать в несжатое растровое изображение для отображения на экране. Небольшой сжатый файл изображения может превратиться в очень большое растровое изображение.

Например, большинство растровых изображений используют конфигурацию ARGB_8888 , что означает, что каждому пикселю требуется 4 байта памяти — по одному байту для красного, зеленого, синего и альфа-канала (прозрачности). Если у вас есть JPEG-файл размером 100 КБ, и вы отображаете его в окне 1000×1000 пикселей, то для каждого из этих 1 000 000 пикселей потребуется 4 байта, что в сумме составит 4 МБ памяти.

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

Отслеживание доступной памяти и её использования.

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

Профилировщик памяти также интегрирован с библиотекой обнаружения утечек LeakCanary . Используя LeakCanary, вы можете перенести анализ утечек памяти с тестового устройства на свою машину для разработки, что может значительно ускорить рабочий процесс. Для получения дополнительной информации см. примечания к выпуску Android Studio .

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

Освобождение памяти в ответ на события

Android может освободить память для вашего приложения или полностью остановить его при необходимости, чтобы освободить память для критически важных задач, как описано в разделе «Обзор управления памятью» . Для дальнейшего балансирования системной памяти и предотвращения необходимости остановки процесса вашего приложения вы можете реализовать интерфейс ComponentCallbacks2 в классах Activity . Предоставленный метод обратного вызова onTrimMemory() уведомляет ваше приложение о событиях жизненного цикла или связанных с памятью событиях, которые предоставляют хорошую возможность для вашего приложения добровольно сократить использование памяти. Освобождение памяти может уменьшить частоту принудительного завершения работы вашего приложения из -за нехватки памяти .

Ваша реализация функции onTrimMemory() должна быть сосредоточена исключительно на событиях TRIM_MEMORY_UI_HIDDEN и TRIM_MEMORY_BACKGROUND . (Начиная с Android 14, система больше не отправляет уведомления для других устаревших констант. Эти константы были официально объявлены устаревшими в Android 15.)

  • TRIM_MEMORY_UI_HIDDEN : Этот сигнал указывает на то, что пользовательский интерфейс вашего приложения вышел из поля зрения пользователя. Этот переход предоставляет возможность освободить значительные объемы памяти, непосредственно связанные с пользовательским интерфейсом, такие как растровые изображения, буферы воспроизведения видео или сложные ресурсы анимации.

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

В этом примере кода показано, как реализовать функцию обратного вызова onTrimMemory() для реагирования на различные события, связанные с памятью:

Котлин

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

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * 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.
        }
    }
}

Java

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

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

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

        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.
        }
    }
}

Проверьте, сколько памяти вам нужно.

Чтобы обеспечить возможность запуска нескольких процессов одновременно, Android устанавливает жесткое ограничение на размер кучи, выделяемый для каждого приложения. Точное ограничение размера кучи варьируется в зависимости от устройства и общего объема доступной оперативной памяти. Если ваше приложение достигает предела емкости кучи и пытается выделить больше памяти, система выдает ошибку OutOfMemoryError .

Чтобы избежать нехватки памяти, вы можете запросить у системы информацию о том, сколько места в куче доступно на текущем устройстве. Запросить эту информацию можно, вызвав getMemoryInfo() . Он возвращает объект ActivityManager.MemoryInfo , который предоставляет информацию о текущем состоянии памяти устройства, включая доступную память, общий объем памяти и пороговый уровень памяти — уровень памяти, при котором система начинает останавливать процессы. Объект ActivityManager.MemoryInfo также предоставляет lowMemory , который представляет собой простое логическое значение, указывающее, не хватает ли памяти на устройстве.

Приведенный ниже пример кода демонстрирует, как использовать метод getMemoryInfo() в вашем приложении.

Котлин

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

Мониторинг ошибок, связанных с нехваткой памяти

Заметные для пользователя завершения процессов при низком уровне памяти (LMK) происходят, когда системная память становится критически низкой. При низком уровне памяти демон lmkd (демон завершения процессов при низком уровне памяти) завершает процессы на основе их oom_adj_score . Приложения, которые кэшируются или запускают службу без связанного пользовательского интерфейса (например, задание), имеют самые высокие значения и завершаются первыми. Если уровень памяти остается критически низким, демон вынужден освобождать память у процессов со значением oom_adj_score равным 0. Поскольку это значение зарезервировано для видимых приложений, их завершение приводит к немедленному, некорректному завершению процесса. Для конечного пользователя это выглядит как сбой приложения, часто обходящий стандартные механизмы сохранения состояния жизненного цикла и приводящий к потере прогресса пользователя.

В Android Vitals основное внимание уделяется завершению процессов, находящихся на переднем плане, поскольку это высокоточный индикатор некорректного управления памятью. Хотя любой показатель LMK выше 1% указывает на критическую необходимость немедленных действий, низкий показатель не обязательно свидетельствует о состоянии системы. Низкий показатель LMK, воспринимаемый пользователем, может означать, что демон LMK часто завершает процессы, находящиеся в фоновом режиме, что ухудшает производительность при «теплом старте» и плавность многозадачности. Поэтому мы рекомендуем придерживаться лучших практик использования памяти независимо от текущего показателя LMK, чтобы обеспечить долгосрочную стабильность и работоспособность устройства.

Используйте ProfilingManager для отслеживания проблем с памятью.

Платформа Android предоставляет ProfilingManager — расширенный API для мониторинга, позволяющий собирать пользовательские данные в рабочей среде на основе заданных вами триггеров. Это может помочь выявить трудновоспроизводимые проблемы с памятью.

Два новых триггера, появившиеся в Android 17 , особенно полезны для выявления проблем с памятью:

  • TRIGGER_TYPE_OOM указывает на то, что приложение вызвало ошибку OutOfMemoryError . Она срабатывает при следующем запуске приложения после сбоя, когда приложение регистрируется для запуска профилирования.
  • TRIGGER_TYPE_ANOMALY срабатывает, когда система обнаруживает аномальное поведение приложения. В частности, это может быть вызвано чрезмерным использованием памяти. TRIGGER_TYPE_ANOMALY срабатывает после того, как приложение продемонстрировало чрезмерное использование памяти, и до того, как система предпримет какие-либо действия для остановки проблемного процесса. Например, если приложение превышает лимиты памяти, введенные в Android 17 , TRIGGER_TYPE_ANOMALY срабатывает перед тем, как система завершит работу приложения.

Для получения дополнительной информации об использовании ProfilingManager для программной регистрации и получения триггеров см. документацию по профилированию на основе триггеров .

Вы также можете использовать профилирование, управляемое приложением, для ручного определения начальной и конечной точек трассировки. Мы рекомендуем делать это для ручного создания дампов или профилей кучи в областях, где вы подозреваете утечки памяти или чрезмерное использование памяти.

Используйте более эффективные с точки зрения использования памяти конструкции кода.

Некоторые функции Android, классы Java и конструкции кода используют больше памяти, чем другие. Вы можете минимизировать объем памяти, используемый вашим приложением, выбирая более эффективные альтернативы в своем коде.

Пользуйтесь услугами экономно.

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

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

Как правило, следует избегать использования постоянных служб из-за постоянной нагрузки на доступную память. Вместо этого мы рекомендуем использовать альтернативную реализацию, например WorkManager . Дополнительную информацию о том, как использовать WorkManager для планирования фоновых процессов, см. в разделе «Постоянная работа» .

Используйте оптимизированные контейнеры данных.

Некоторые классы, предоставляемые языком программирования, не оптимизированы для использования на мобильных устройствах. Например, универсальная реализация HashMap может быть неэффективной с точки зрения использования памяти, поскольку для каждого отображения требуется отдельный объект записи.

Фреймворк Android включает в себя несколько оптимизированных контейнеров данных, в том числе SparseArray , SparseBooleanArray и LongSparseArray . Например, классы SparseArray более эффективны, поскольку они избавляют систему от необходимости автоматической упаковки ключа, а иногда и значения, что приводит к созданию еще одного или двух объектов для каждой записи.

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

Будьте осторожны с абстракциями кода.

Разработчики часто используют абстракции как хорошую практику программирования, поскольку они могут повысить гибкость кода и упростить его сопровождение. Однако абстракции, как правило, требуют выполнения большего объема кода. Как подробно описано в статье «Сокращение объема кода и ресурсов вашего приложения» , больший объем скомпилированного кода напрямую увеличивает объем физической оперативной памяти, необходимой вашему приложению. Если ваши абстракции не приносят существенной пользы, избегайте их.

Используйте облегченные протобуфы для сериализованных данных.

Протоколы буферизации (protobufs) — это независимый от языка программирования, платформы и расширяемый механизм, разработанный Google для сериализации структурированных данных — похожий на XML, но меньшего размера, более быстрый и простой. Если вы используете protobufs для своих данных, всегда используйте облегченные версии protobufs в клиентском коде. Обычные protobufs генерируют чрезвычайно многословный код, что увеличивает объем кода вашего приложения в оперативной памяти (см. раздел «Управление и оптимизация объема кода вашего приложения ») и способствует увеличению размера APK-файла.

Для получения более подробной информации см. файл readme протокола protobuf .

Будьте осторожны с утечками памяти.

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

Для получения более подробной информации см. раздел «Утечки памяти» .

Избегайте перегрузки памяти.

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

Часто обновление памяти может приводить к большому количеству событий сборки мусора. На практике обновление памяти описывает количество выделенных временных объектов, которые появляются за определенный промежуток времени.

Например, вы можете выделить несколько временных объектов внутри цикла for . Или вы можете создать новые объекты Paint или Bitmap внутри функции onDraw() представления. В обоих случаях приложение быстро создает множество объектов в большом объеме. Это может быстро занять всю доступную память в молодом поколении, что приведет к запуску сборки мусора.

Используйте профилировщик памяти , чтобы найти в вашем коде места с высокой частотой использования памяти, прежде чем вы сможете их исправить.

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

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

Тщательно оцените производительность, чтобы определить, подходит ли пул объектов в данной ситуации. В некоторых случаях пулы объектов могут ухудшить производительность. Хотя пулы и позволяют избежать выделения памяти, они вводят другие накладные расходы. Например, поддержание пула обычно включает синхронизацию, которая имеет существенные накладные расходы. Кроме того, очистка экземпляра объекта в пуле во избежание утечек памяти во время освобождения и последующая инициализация во время получения могут иметь ненулевые накладные расходы.

Сохранение в пуле большего количества экземпляров объектов, чем необходимо, также создает нагрузку на сборщик мусора. Хотя пулы объектов уменьшают количество вызовов сборки мусора, в конечном итоге они увеличивают объем работы, необходимой для каждого вызова, поскольку он пропорционален количеству доступных (активных) байтов.