Введение / Зачем это нужно
Профилирование памяти — ключевой этап в разработке стабильных Android-приложений. Утечки памяти (memory leaks) приводят к постепенному росту потребления RAM, замедлению работы, а в худшем случае — к крашу с ошибкой OutOfMemoryError. Встроенный Memory Profiler в Android Studio позволяет визуально отслеживать распределение памяти в реальном времени, делать снимки состояния кучи (Heap Dump) и находить объекты, которые не должны находиться в памяти. Этот гайд проведёт вас через весь процесс от запуска инструмента до интерпретации результатов.
Требования / Подготовка
Перед началом убедитесь, что:
- У вас установлен Android Studio версии Hedgehog (2023.1.1) или новее. Инструмент постоянно улучшается.
- Устройство или эмулятор работает на Android 5.0 (API 21) или выше.
- Приложение собрано в debug-конфигурации (с включённой отладочной информацией). Для release-сборок анализ возможен, но сложнее.
- В
build.gradle(module) у вашего приложения включёнdebuggable trueдля debug-сборок (обычно это по умолчанию). - Устройство подключено по USB с включённой отладкой по USB, или запущен эмулятор.
Шаг 1: Запустите Profiler и подключите устройство
- Запустите ваше приложение на устройстве или эмуляторе из Android Studio (кнопка
Run). - Откройте окно Profiler:
View → Tool Windows → Profilerили используйте сочетание клавишAlt+Shift+F10(Windows/Linux) /⌥⇧F10(macOS). - В верхней части окна Profiler вы увидите список активных процессов. Выберите процесс вашего приложения (обычно с именем пакета, например,
com.example.myapp). - По умолчанию откроется вкладка CPU. Переключитесь на вкладку Memory (иконка с синим графиком и цифрами).
На графике вы увидите реальное потребление памяти вашим приложением:
- Java Heap: память, управляемая JVM/ART (ваш код).
- Native Heap: память, выделенная через NDK (C/C++ код).
- Graphics: память, используемая для буферов отрисовки.
- Stack: память под стековые фреймы.
- Code: память под скомпилированный код.
- Others: прочее.
Шаг 2: Начните запись и выполните сценарий
- На панели инструментов вкладки Memory нажмите кнопку
Record memory allocations(иконка с красной точкой внутри кружка). Статус записи изменится наRecording.... - Выполните в приложении действие, которое, как вы подозреваете, вызывает утечку. Например:
- Откройте и закройте фрагмент/активность несколько раз.
- Прокрутите список (
RecyclerView) с большим количеством элементов. - Перейдите на другой экран и вернитесь.
- Важно: пытайтесь воспроизвести сценарий, который пользователь мог бы выполнить многократно. Утечка часто проявляется после нескольких итераций.
- После выполнения действия нажмите кнопку
Stop recording(та же кнопка, теперь красная). Запись выделений объектов прекратится.
💡 Совет: Не записывайте слишком долго — это может привести к огромному объёму данных и замедлению работы. 30-60 секунд активных действий обычно достаточно.
Шаг 3: Сделайте Heap Dump для анализа
Запись allocations полезна для отслеживания частоты создания объектов, но для поиска утечек нужен Heap Dump — полный снимок объектов в куче в конкретный момент времени.
- Убедитесь, что вы находитесь на вкладке Memory.
- Нажмите кнопку
Dump Java heap(иконка с кучей мусора и стрелкой вниз). Профилер запросит у системы создание дампа. - Дождитесь завершения. На экране появится новая вкладка
Heap Dumpс результатами.
Шаг 4: Проанализируйте результаты в Analyzer
Вкладка Heap Dump автоматически открывает анализатор. По умолчанию вы видите таблицу Classes.
Как читать таблицу:
Class— имя класса объекта.Instance Count— количество экземпляров этого класса в куче на момент дампа.Size— общий объём памяти, занятый всеми экземплярами этого класса (в КБ/МБ).Retained Size— самый важный показатель. Это объём памяти, который будет освобождён, если все объекты этого класса (и те, на которые они ссылаются) будут удалены. БольшойRetained Sizeу класса, который не должен быть в памяти — верный признак утечки.
Первое действие: отсортируйте таблицу по столбцу Retained Size (по убыванию). Вверху окажутся классы, удерживающие наибольшие объёмы памяти.
Что ищем:
- Классы вашего приложения (например,
MyActivity,MyAdapter,MyViewModel), которые не должны существовать после закрытия экрана/фрагмента. - Классы из библиотек, известные своими утечками (например, старые версии
androidx.lifecycle.ViewModelпри неправильной конфигурации). - Большое количество экземпляров
Bitmap,Context,View,Activity,Fragment.
Шаг 5: Исследуйте пути до GC Root
Нашли подозрительный класс? Теперь нужно понять, почему он не удаляется сборщиком мусора (Garbage Collector). Для этого ищут GC Root — объект, который напрямую или через цепочку ссылок удерживает ваш объект в памяти.
- В таблице
Classesнайдите подозрительный класс и выделите его (кликните на строку). - На панели справа (или в контекстном меню) выберите
Show Path to GC Roots. - Появится диалоговое окно с опциями. Выберите
Show all paths(илиShow soft references, если подозреваете кэши). НажмитеOK. - Анализатор построит дерево ссылок от GC Root (вверху) к вашему объекту (внизу).
Как интерпретировать:
- GC Root — это обычно системный объект (например,
android.app.ActivityThread,java.lang.Thread,android.view.ViewRootImpl), или статическое поле вашего класса (MyClass.sStaticField). - Ищите в цепочке контекст (
Context) активности, который, например, хранится в статическом поле или долгоживущем синглтоне. Это классическая утечка активности. - Если в цепочке есть ваш
SingletonилиObjectиз кэша — значит, он не очищается при уничтожении экрана.
⚠️ Важно: Не все пути до GC Root — это утечки. Если объект действительно нужен для работы приложения (например, сервис), его наличие оправдано. Ищите объекты, которые должны были быть уничтожены (закрытые активности, фрагменты).
Проверка результата
После внесения исправлений в код (например, очистки ссылок в onDestroy(), использования WeakReference, замены статических контекстов на ApplicationContext) повторите шаги 1-4.
Критерии успеха:
- На графике Memory Profiler после закрытия проблемного экрана память (Java Heap) возвращается к базовому уровню, а не растёт.
- В новом Heap Dump количество экземпляров подозрительного класса (
Instance Count) значительно сократилось или равно нулю. - В анализаторе
Retained Sizeдля этого класса стал близок к нулю. - Путь до GC Root для этого класса больше не существует или ведёт к объекту, который закономерно живёт всё время работы приложения.
Возможные проблемы
1. Profiler не показывает процесс приложения
- Причина: Приложение собрано в release-конфигурации без debuggable флага, или устройство не авторизовано для отладки.
- Решение: Запустите debug-сборку. На устройстве в
Настройки → Для разработчиковубедитесь, что включена Отладка по USB и вы подтвердили RSA-ключ компьютера.
2. Heap Dump создаётся очень долго или падает с ошибкой
- Причина: Слишком большое количество объектов в куче (десятки тысяч), нехватка памяти на компьютере для анализа.
- Решение:
- Упростите сценарий, делайте дамп раньше.
- Закройте другие тяжёлые приложения в эмуляторе/устройстве.
- Увеличьте оперативную память, выделенную эмулятору (в AVD Manager).
- Используйте фильтры в анализаторе (поле
Filter), чтобы смотреть только ваши пакеты (например,com.example).
3. Не могу найти проблемный класс — в списке только системные
- Причина: Утечка может быть в нативном коде (Native Heap) или в объектах, которые Profiler не может корректно отобразить (например, из-за прозрачных обёрток).
- Решение:
- Переключитесь на вкладку
Nativeв Profiler и посмотрите рост native heap. - Используйте
adb shell dumpsys meminfo <package_name>в терминале для более низкоуровневого отчёта. - Если используете библиотеки с нативным кодом (например, OpenGL, FFmpeg), проверьте их документацию на предмет известных утечек.
- Переключитесь на вкладку
4. После исправления утечки график памяти всё равно растёт
- Причина: У вас не одна, а несколько утечек, или рост связан с закономерным кэшированием данных (например,
LruCache). - Решение:
- Сравните несколько Heap Dumps, сделанных с интервалом. Посмотрите, какие классы увеличиваются в количестве между дампами.
- Проверьте, не является ли рост линейным и предсказуемым (например, добавление элементов в
ArrayListбез очистки). Это может быть не утечкой, а бизнес-логикой. - Проанализируйте
Allocation Tracker(кнопкаRecord allocations), чтобы увидеть, где создаются новые объекты.
5. Путь до GC Root ведёт через системный класс, который я не контролирую
- Причина: Некоторые системные классы (например,
InputMethodManager) исторически содержат утечки через неочищаемые ссылки. - Решение:
- Проверьте, не держите ли вы сами ссылку на
ViewилиContextв статическом поле или долгоживущем объекте. - Для известных системных утечек есть проверенные паттерны: например, в
onDestroy()активности вызыватьinputMethodManager.isActive = falseили очищать фокус уEditText. - Поищите конкретный случай (класс + "memory leak") в документации Android или на Stack Overflow.
- Проверьте, не держите ли вы сами ссылку на