В этой статье я покажу, что утечки памяти в Android приводят к замедлениям, зависаниям и ANR чаще, чем к OutOfMemoryError сбоям.
Задержка при навигации
В Square мы отслеживаем метрики производительности, ориентированные на пользователей — в частности, задержку взаимодействия. Мы отслеживаем этот показатель для каждой навигации в приложении (пример реализации: «Время реакции на тап: Jetpack Navigation«).
Другими словами, для каждой навигации мы сообщаем метрику задержки, которая измеряет время от момента получения касания до момента обновления экрана, т.е. насколько велика задержка, воспринимаемая пользователями.
val durationMillis = frameCommitted - actionUpMotionEvent.eventTime analytics.logNavigation( originScreen, destinationScreen, durationMillis )
Использование памяти при навигации
Такие показатели потребления ресурсов, как использование памяти, часто представляются в виде временных рядов, что не очень удобно при попытке соотнести использование приложений с утечками памяти.
В январе 2023 года Павел Ставицкий опубликовал в блоге Lyft Engineering статью “Обнаружение утечек памяти в Android в продакшене”.
Одна из интересных идей статьи заключается в том, чтобы сообщать метрики использования памяти при каждой навигации по экрану, а не в виде временного ряда, поскольку утечки памяти имеют тенденцию накапливаться по мере использования приложения.
Давайте обновим нашу аналитику навигации, чтобы добавить данные об использовании памяти:
val runtime = Runtime.getRuntime() val javaHeapUsage = runtime.totalMemory() - runtime.freeMemory() analytics.logNavigation( sourceScreen, destinationScreen, durationMillis, javaHeapUsage )
Ограничение памяти
Если бы Android-устройства обладали бесконечной памятью, утечки памяти не были бы проблемой. Но на устройствах с Android оперативная память ограничена, каждому приложению разрешено использовать только часть оперативной памяти устройства для своей Java-кучи, и утечки памяти становятся проблемой, когда использование памяти приближается к пределу. Этот предел настраивается для каждого устройства и может быть запрошен с помощью Runtime.maxMemory():
val javaHeapLimit = Runtime.getRuntime().maxMemory() analytics.logNavigation( sourceScreen, destinationScreen, durationMillis, javaHeapUsage, javaHeapLimit )
Пример протекающей сессии
Мы можем построить график использования памяти с течением времени для одного сессии, где каждая точка данных — это одна навигация. Вот реальный пример сессии с 1591 переходом, где мы видим, что использование памяти растет с течением времени:
Обратите внимание, что использование кучи Java постоянно скачет вверх и вниз по мере работы GC, но тенденция к росту указывает на утечку памяти. Применение линейной регрессии показало, что наклон составляет +146 КБ на навигацию.
Использование памяти и задержка при навигации
Добавим к графику задержку навигации:
Обратите внимание, что задержка при навигации остается довольно ровной на протяжении всей сессии, пока использование памяти не приблизится к пределу, и тогда задержка при навигации резко возрастает. Мы можем увеличить масштаб последних 200 переходов:
В этом примере пользовательский интерфейс замирает на несколько секунд, пока GC блокирует основной поток для освобождения памяти. Это начинает происходить, когда память приближается к пределу в 18 МБ.
Прогрессирующее влияние утечек памяти
По мере приближения памяти кучи Java к лимиту памяти приложения, влияние утечек памяти становится все более заметным.
- Сначала небольшие GC-паузы приводят к замиранию анимации.
- Затем более длительные GC-паузы приводят ко все более длительному зависанию пользовательского интерфейса, причем на несколько секунд.
- Если основной поток замирает более чем на 5 секунд в ожидании диспетчеризации тач событий, приложение выдает ошибку Application Not Responding (ANR).
- В конце концов, памяти остается так мало, что мы не можем выделять новые объекты, и приложение завершается с исключением OutOfMemoryError.
Игнорирование реального влияния утечек памяти
Если у вас есть отчетность о сбоях и процесс исправления самых распространенных ошибок, то вы молодцы! К сожалению, по двум причинам нельзя просто смотреть на ошибки OutOfMemoryError, чтобы решить, что следует заняться устранением утечек памяти:
- Инструменты для создания отчетов о сбоях обычно группируют сбои по идентичным трассировкам стека и предоставляют количество сбоев по каждой группе. Но при нехватке памяти ошибка OutOfMemoryError может быть вызвана из любого места кода приложения, что означает, что каждая ошибка OutOfMemoryError потенциально оставляет свой собственный след в стеке. Вместо одной записи о сбое на 1000 сбоев, сбои OutOfMemoryError регистрируются как 1000 отдельных сбоев и скрываются в длинном хвосте мало повторяющихся сбоев.
- Поскольку приложение замедляется и замирает на несколько секунд, пользователи мобильных устройств либо перестают им пользоваться, либо завершают его работу и запускают заново. Таким образом, приложение может никогда не аварийно завершаться из-за OutOfMemoryError, хотя последствия для пользователей вполне реальны.
Выводы
- Утечки памяти в Android постепенно приводят к замедлениям, затем к зависаниям, затем к ANR и, в конце концов, к сбоям OutOfMemoryError (если пользователь еще не вышел из приложения или не убил его).
- Если отчет ANR показывает стек-трассировку, которая не похожа на ту, которая действительно может вызвать ANR, следует обратить внимание на использование памяти и ее лимит. Если память близка к пределу, то ANR, скорее всего, происходит из-за того, что GC блокирует основной поток.
- Чтобы избежать подобных проблем с производительностью, необходимо систематически устранять все утечки памяти, обнаруженные LeakCanary.
- Комбинируя данные об использовании и ограничении памяти с данными о производительности в продакшене, можно выявить взаимосвязь между утечками памяти и производительностью.
- Хотя я не могу поделиться фактическими цифрами, мы увидели прямую зависимость между активностью пользователей, количеством утечек и частотой зависаний/ANR.
- Линейная регрессия использования памяти от количества переходов за сессию может показать, есть ли в сессии утечка памяти и насколько она серьезная.