В нашем ежедневном стремлении к созданию лучших приложений мы, как разработчики, должны принимать во внимание многие вещи, чтобы не сбиться с пути, одна из которых - убедиться, что наши приложения не дают сбоев. Частая причина сбоев - утечки памяти. Эта конкретная проблема может проявляться в различных формах. В большинстве случаев мы видим устойчивое увеличение использования памяти до тех пор, пока приложение не сможет выделить больше ресурсов и неизбежно выйдет из строя. В Java это часто приводит к возникновению исключения OutOfMemoryException. В некоторых редких случаях просочившиеся классы могут даже оставаться достаточно долго, чтобы получать зарегистрированные обратные вызовы, вызывая некоторые действительно странные ошибки и слишком часто вызывая печально известное исключение IllegalStateException.

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

Отказ от ответственности

Цель этого поста и примеров кода - способствовать более глубокому пониманию управления памятью, особенно в Java. Показанная общая архитектура, управление потоками и обработка запущенных HTTP-запросов не идеальны для производственной среды и просто служат носителем заявленной мысли: необходимо учитывать утечки памяти в Android.

Регистрация слушателей

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

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

Но чтобы исправить утечку памяти, мы сначала должны найти ее. К счастью, в Android Studio есть встроенный инструмент под названием Android Monitor, который мы можем использовать, помимо прочего, для наблюдения за использованием памяти. Все, что нам действительно нужно сделать, это открыть Android Monitor и перейти на вкладку Monitors, чтобы увидеть, сколько памяти используется и выделяется в режиме реального времени.

Здесь будут отражены любые взаимодействия, вызывающие выделение ресурсов, что делает его идеальным местом для отслеживания использования ресурсов вашим приложением. Чтобы найти утечку памяти, нам нужно знать, что она содержит в тот момент времени, когда мы подозреваем, что произошла утечка памяти. В этом конкретном примере все, что нам нужно сделать, это запустить наше приложение, повернуть устройство один раз и затем вызвать действие Дамп Java Heap (рядом с Память, третий значок из левый). Это сгенерирует файл hprof, который содержит моментальный снимок памяти на момент вызова действия. Через пару секунд Android Studio автоматически открывает файл, предоставляя нам аккуратное визуальное представление памяти для удобного анализа.

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

Если мы выберем просочившуюся активность, нам будет представлено Дерево ссылок, в котором можно идентифицировать ссылку, которая поддерживает активность. При поиске экземпляров с нулевой глубиной мы обнаруживаем, что экземпляр mListener, расположенный в диспетчере местоположения, является причиной того, что наша деятельность не может быть обработана сборщиком мусора. Возвращаясь к нашему коду, мы видим, что эта ссылка связана с requestLocationsUpdates, где мы устанавливаем действие как обратный вызов для обновлений местоположения. Изучив документацию по диспетчеру местоположения, быстро становится ясно, что для отключения ссылки мы должны просто вызвать метод removeUpdates. В нашем примере, поскольку мы регистрируемся для получения обновлений в методе onCreate, очевидным местом для отмены регистрации будет метод onDestroy.

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

Внутренние классы

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

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

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

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

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

Кажется, нам еще есть над чем поработать. Применяя ту же технику, что и в последнем примере, мы можем идентифицировать выделенный экземпляр в ссылочном дереве как тот, который поддерживает активность. Так что здесь происходит? Если мы внимательно посмотрим на его родительский узел, мы увидим, что в представлении есть ссылка на mContext, которая является не чем иным, как нашей просочившейся активностью. Итак, как это решить? Мы не можем исключить контекст, к которому привязано представление, и нам нужна ссылка на представление в BackgroundTask, чтобы обновить пользовательский интерфейс. Один простой способ решить эту проблему - использовать WeakReference. Наша ссылка на resultTextView считается сильной и позволяет поддерживать экземпляр в рабочем состоянии, предотвращая сборку мусора. Напротив, WeakReference не будет поддерживать активный экземпляр, на который указывает ссылка. Как только последняя сильная ссылка на экземпляр будет удалена, сборщик мусора восстановит свои ресурсы независимо от любых слабых ссылок на этот объект. Вот как будет выглядеть окончательная версия с использованием WeakReference:

Обратите внимание, что в onPostExecute мы должны проверить наличие null, чтобы проверить, был ли экземпляр возвращен или нет.

Наконец, повторный запуск задачи анализатора подтвердит, что наша активность больше не просачивается!

Анонимные классы

Эти типы классов имеют те же недостатки, что и внутренний класс, а именно, они содержат ссылку на включающий класс. Как и внутренние классы, анонимные классы могут быть источниками утечек памяти при передаче экземпляру, который работает вне жизненного цикла активности или выполняет работу в другом потоке. В этом примере я буду использовать популярный HTTP-клиент Retrofit, чтобы выполнить вызов API и передать ответ на обратный вызов. Клиент настраивается аналогично примеру на Домашней странице Retrofit. Я также сохраню ссылку на GitHubService в экземпляре приложения, что не особенно хорошо подходит для дизайна, но служит целям этого примера.

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

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

Запуск задачи анализатора в этом решении не приведет к утечке действий.

Заключение

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

  • Предпочитайте статические внутренние классы нестатическим. Каждый нестатический внутренний класс будет иметь неявную ссылку на свой экземпляр внешнего класса, что может привести к нежелательному поведению. Вместо этого определите эти классы как статические и сохраните необходимые ссылки жизненного цикла как WeakReferences.
  • Рассмотрите другие средства фонового обслуживания. Android предлагает множество способов для выхода из основного потока, например HandlerThread, IntentService и AsyncTask, у каждого свои сильные и слабые стороны. Вместе с тем Android предлагает несколько механизмов для передачи информации обратно в основной поток для обновления пользовательского интерфейса. BroadcastReceiver - очень способный инструмент для этого.
  • Не полагайтесь слепо на сборщик мусора. При кодировании на языке сборщика мусора легко сделать предположение, что управление памятью не нужно принимать во внимание. Наши примеры ясно показывают, что это не так. Поэтому убедитесь, что все выделенные вами ресурсы собраны должным образом.

Хотите узнать больше?

Вы только начинаете работать с памятью и управлением производительностью в Android? Затем изучите следующие ресурсы, чтобы получить дополнительную информацию о том, как создавать лучшие приложения и стать лучшим разработчиком Android.

ОБНОВЛЕНИЕ. С помощью нескольких дружеских замечаний от сообщества Android я скорректировал пример кода в соответствии с общепринятыми рекомендациями.

Интересуетесь, как мы разрабатываем программное обеспечение? Присоединяйтесь к нашей команде в Гамбурге, Германия!