Android application memory leak

Утечка памяти

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

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

Память условно имеет две области для хранения данных — стек (stack) и куча (heap).

Стек работает в порядке LIFO (Last In, First Out), то есть последний добавленный в стек фрагмент данных будет первым в очереди на вывод из стека. Каждый раз, когда функция объявляет новую переменную, переменная добавляется в стек, а когда эта переменная пропадает из области видимости, она автоматически удаляется из стека. Когда стековая переменная освобождается, эта область памяти становится доступной для других стековых переменных. Размер стека — это фиксированная величина, и превышение лимита выделенной на стеке памяти приведёт к переполнению стека. Размер задаётся при создании потока, и у каждой переменной есть максимальный размер, зависящий от типа данных.

Куча — это хранилище памяти, также расположенное в ОЗУ, которое допускает динамическое выделение памяти. Куча не имеет упорядоченного набора данных, это просто склад для ваших переменных. По завершении приложения все выделенные участки памяти освобождаются. Размер кучи задаётся при запуске приложения, но, в отличие от стека, он ограничен лишь физически, и это позволяет создавать динамические переменные.

Чтобы наглядно преддставить способ хранения объектов в памяти, напишем простую программу на Java.

Размещение в памяти при запуске выглядит следующим образом.

По рисунку видно, что в стек попали функция main(), переменная с примитивным типом int.

Также в стек попадает объект obj, когда он создаётся из класса Object, при этом в куче создаётся ссылка на класс (указатель).

Аналогично, в стеке появляется объект mem с ссылкой на класс в куче.

Для функции foo() в стеке создаётся отдельный блок. В этом блоке создаётся объект param с ссылкой в куче на класс Object и строковый объект с ссылкой в куче на отдельный блок String Pool.

Когда в программе выполнение доходит до закрывающей фигурной скобкой метода foo(), метод прекращает работу и объекты в стеке, относящиеся к блоку функции, освобождаются. Память выглядит следующим образом.

Последняя закрывающая фигурная скобка от функции main() закрывает эту функцию, освобождая свой блок данных.

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

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

Сначала ответим на вопрос: а зачем исправлять эти ошибки, чем это грозит? Даже с утечками памяти приложение может работать.

Читайте также:  Syphon filter для андроид

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

Основные проблемные источники: Context и его производные (Activity), внутренние классы (Inner Classes), анонимные классы (Anonymous Classes), Handlers c Runnable, Threads, TimerTask, SensorManager и другие менеджеры.

Самая главная рекомендация — никогда не сохраняйте ссылки на Context, Activity, View, Fragment, Service в статических переменных.

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

Почему же утечка активности такая большая проблема? Дело в том, что если сборщик мусора не соберёт Activity, то он не соберёт и все View и Fragment, а вместе с ними и все прочие объекты, расположенные в Activity. В том числе не будут высвобождены картинки. Поэтому утечка любой активности — это, как правило, самая большая утечка памяти, которая может быть в вашем приложении.

Используйте передачу объектов через Intent, либо передавайте ID объекта (если у вас есть база данных, из которой этот id потом можно достать).

Этот пункт также относится к любым объектам, временем жизни которых напрямую или косвенно управляет Android, т.е. View, Fragment, Service и т.д.

Объекты View и Fragment содержат ссылку на Activity, в котором они расположены, поэтому, если утечёт один единственный View, утечёт сразу всё — Activity и все View в ней. И заодно все drawable и всё, на что у любого элемента из экрана есть ссылка!

Будьте аккуратны при передаче ссылки на Activity (View, Fragment, Service) в другие объекты.

Утечка через слушателей

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

Как добиться в этой ситуации утечки памяти? Тоже очень несложно! Просто забудьте отписаться от уведомлений в методе onPause():

Из-за такой утечки памяти активность будет продолжать обновлять интерфейс каждый раз, когда профиль будет обновляться даже после того, как экран перестанет быть видим пользователю. Хуже того, таким образом экран может подписать 2, 3 или больше раза на одно и то же уведомление. Это может привести к видимым тормозам интерфейса в момент обновления профиля — и не только на этом экране.

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

Вы можете сохранять не ссылки на объекты, а слабые ссылки. Это особенно полезно для наследников класса View — ведь у них нет метода onPause() и не совсем понятно, в какой момент они должны отписываться от уведомления. Слабые ссылки не считаются сборщиком мусора как связи между объектами, поэтому объект, на который существуют только слабые ссылки, будет уничтожен, а ссылка перестанет ссылаться на объект и примет значение null.

Читайте также:  Как ускорить android смартфон

Другой пример с использованием системных слушателей. Например, есть слушатель определения местоположения.

Если забудем снять регистрацию слушателя в onStop(), то пользователь может закрыть приложение, но сборщик мусора не сможет освободить память, так как LocationManager будет по-прежнему выполнять свою работу.

Пример утечки с внутренним классом

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

Сам пример вполне нормальный. Но нужно помнить, что класс BackgroundTask хранит ссылку на активность. Если задача выполняется очень долго (плохое соединение, большая картинка на сервере), то сложная активность со всеми своими ресурсами остаётся в памяти, пока задача не будет отработана.

Есть разные варианты решения задачи. Часто рекомендуют подход с WeakReference.

Код усложнился, кроме того, вам придётся изучать устройство WeakReference.

Для примера на Kotlin можно убрать модификатор inner.

Если в качестве внутреннего класса использовать Handler, то студия будет выводить подсказку This Handler class should be static or leaks might occur (anonymous android.os.Handler). Код, чтобы увидеть подсказку.

Более подробное описание подсказки в студии:

Пример утечки с анонимным классом

Принцип утечки памяти схож с примером с внутренним классом, когда сохраняется ссылка на активность. Экземпляр анонимного класса живёт дольше, чем контейнер. Если анонимный класс вызывает какой-то метод, читает или записывает свойство в класс-контейнер, то он держит в памяти класс-контейнер.

Утечка через потоки

Случай первый — потоки. Создадим внутренний класс внутри активности. Внутренний класс будет иметь ссылку на активность.

В обычной ситуации пользователь запустит активность, запустится задача на двадцать секунд.

Когда задача выполнится, стек освободит объекты.

Затем сборщик мусора освободит объекты в куче.

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

Рассмотрим случай, когда пользователь закроет активность или повернёт экран после десяти секунд.

Задача по-прежнему выполняется, ссылка на активность по-прежнему жива и мы имеем утечку памяти.

Когда метод run() выполнится, стек освободит объекты и сборщик мусора в порядке очереди почистит объекты в куче, так как они уже не будут иметь ссылок из стека.

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

Singleton (Одиночка)

Когда происходит утечка памяти? Когда мы инициализируем синглтон в активности, то передаём ссылку на контекс-активность с долгим сроком жизни.

В этом случае ссылка на активность будет существовать пока не закроется приложение.

Чтобы избежать утечку, используйте контекст приложения, а не активности.

Или вы можете переписать класс одиночки.

Утечка с таймерами

Таймеры, которые не отменяются при выходе с экрана, тоже служат источниками утечки памяти.

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

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

Фрагменты

Никогда не сохраняйте ссылки на Fragment в активности или другом фрагменте.

Активность хранит ссылки на 5-6 запущенных фрагментов даже если на экране всегда виден только один. Один фрагмент хранит ссылку на другой фрагмент. Фрагменты, видимые на экране в разное время, общаются друг с другом по прямым закешированным ссылкам. FragmentManager в таких приложениях выполняет чаще всего рудиментарную роль — в нужный момент он подменяет содержимое контейнера нужным фрагментом, а сами фрагменты в back stack не добавляются (добавление фрагмента, на который у вас есть прямая ссылка, в back stack рано или поздно приведёт к тому, что фрагмент будет выгружен из памяти; после возврата к этому фрагменту будет создан новый, а ваша ссылка продолжит ссылаться на существующий, но невидимый пользователю фрагмент).

Читайте также:  Rdp как подключиться с андроид

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

Во-вторых, эти фрагменты становится крайне сложно переиспользовать. Попробуйте перенести фрагмент в другое место программы при условии, что он должен быть обязательно запущен в одном Activity с фрагментами, x, y и z, которые переносить не надо.

Относитесь к фрагментам как к Activity. Делайте их максимально модульными, общайтесь между фрагментами только через Activity и FragmentManager.

Рассмотренные примеры — это частные случаи одного общего правила. Все утечки памяти появляются тогда и только тогда, когда вы сохраняете ссылку на объект с коротким жизненным циклом (short-lived object) в объекте с длинным жизненным циклом (long-lived object).

OutOfMemoryError — частая причина падения приложения из-за нехватки памяти. Особенно, если вы активно используете изображения.

Утечки памяти, связанные с неправильным использованием android.os.Handler. Не совсем очевидно, но все, что вы помещаете в Handler, находится в памяти и не может быть очищено сборщиком мусора в течении некоторого времени. Иногда довольно длительного. Читайте статью Борьба с утечками памяти в Android. Часть 1

Пример утечки с системными менеджерами

В Android есть много системных менеджеров (содержат слово «Manager» в именах классов), которые следует регистрировать. И часто программисты забывают снять регистрацию.

Возьмём для примера класс LocationManager, который помогает определить местоположение. Напишем минимальный код.

Запустите пример. В студии внизу выберите вкладку 6: Android Monitor (Сейчас вместо него появился Profiler), а в ней вкладку Monitors. В верхней части окна будет блок Memory, который представляет для нас интерес.

Начинайте вращать устройство с запущенным приложением. Вы увидите, что ваше приложение начинает забирать память у устройства (тёмно-синий цвет).

Мне не удалось исчерпать всю память и сломать приложение, в какой-то момент умная система освобождала занятую память и всё повторялось снова. В других ситуациях может случиться так, что память кончится раньше, чем сообразит система.

Нажмите на третью кнопку в этом окне Dump Java Heap. Данное действие сгенерирует hprof-файл, содержащий слепок памяти в заданный момент. Далее студия автоматически откроет созданный файл, который можно изучить.

Обратите внимание на вкладку Analyzer Tasks сбоку в верхнем правом углу. Откройте эту вкладку. В ней вы увидите строчку с флажком Detect Leaked Activities (Обнаружить утекающие активности). В окнеAnalysis Results щёлкните по строке Leaked Activities, чтобы увидеть дополнительную информацию.

Видно, что при поворотах создавалось множество активностей MainActivity, а вместе с ней и объект LocationManager.

Добавим код в метод onDestroy(), как это предписано документацией.

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

В Android Studio есть специальный инструмент, который позволяет следить за памятью — Profiler, запускаемый из меню View | Tool Windows. Также имеется отдельный значок инструмента на панели в верхней части. Новый инструмент заменил Android Monitor в старых версиях студии.

Запустите профайлер, появится окно с четырьмя блоками: CPU, MEMORY, NETWORK, ENERGY. Нас интересует память. Щёлкаем в этой области, чтобы оставить слежение только за используемой памятью.

Нажмите кнопку Dump Java heap, чтобы получить дамп кучи. Рядом имеется кнопка очистки мусора Force garbage collection.

Источник

Оцените статью