Борьба с утечками памяти в Android. Часть 1

в 12:09, , рубрики: android, badoo, memory, mobile development, optimisations, profiling, Блог компании Badoo, Разработка под android

Борьба с утечками памяти в Android. Часть 1 Этой статьей мы открываем цикл статей на Хабре о нашей разработке под Android.
Согласно докладу компании Crittercism от 2012 года, OutOfMemoryError — вторая по распространенности причина «крашей» мобильных приложений.
Честно говоря, и в Badoo эта ошибка была в топе всех крашей (что неудивительно при том объеме фотографий, которые просматривают наши пользователи). Борьба с OutOfMemory — занятие кропотливое. Мы взяли в руки Allocation Tracker и начали играться с приложением. Наблюдая за данными зарезервированной памяти, мы выявили несколько сценариев, при которых выделение памяти росло с подозрительной стремительностью, забывая при этом уменьшаться. Вооружившись несколькими дампами памяти после этих сценариев, мы проанализировали их в MAT (http://www.eclipse.org/mat/).
Результат был занимательный и позволил нам в течение нескольких недель снизить количество крашей в разы. Что-то было специфично для нашего кода, но также выявились типичные проблемы, присущие большинству Android приложений.
Сегодня поговорим о конкретном случае утечки памяти. О нем многие знают, но часто закрывают на это глаза (а зря).

Речь пойдет об утечках памяти, связанных с неправильным использованием android.os.Handler. Не совсем очевидно, но все, что вы помещаете в Handler, находится в памяти и не может быть очищено сборщиком мусора в течении некоторого времени. Иногда довольно длительного.
Чуть позже мы покажем на примерах, что происходит и почему память не может быть освобождена. Если вы не любопытный, но хотите знать, как бороться с проблемой, то перейдите к выводам в конце статьи. Или сразу отправляйтесь на страничку маленькой библиотеки, которую мы выложили в открытый доступ: https://github.com/badoo/android-weak-handler.

Итак, что же там «течет»? Давайте разберемся.

Простой пример

Борьба с утечками памяти в Android. Часть 1

Это очень простой класс Activity. Предположим, что нам нужно поменять текст по прошествии 800 секунд. Пример, конечно, нелепый, но зато хорошо продемонстрирует нам, как текут ручьи нашей памяти.
Обратите внимание на анонимный Runnable, который мы постим в Handler. Так же важно обратить внимание на длительный тайм-аут.
Для теста мы запустили этот пример и повернули телефон 7 раз, тем самым вызвав смену ориентации экрана и пересоздание Activity. Затем сняли дамп памяти и открыли его в MAT (http://www.eclipse.org/mat/).

С помощью OQL запускаем простой запрос, который выводит все инстансы класса Activity:

select * from instanceof android.app.Activity

Мы очень рекомендуем почитать про OQL — он ощутимо поможет вам в анализе памяти.
Почитать можно тут visualvm.java.net/oqlhelp.html или тут help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Freference%2Foqlsyntax.html.

Борьба с утечками памяти в Android. Часть 1

В памяти висит 7 инстансов Activity. Это в 7 раз больше, чем нужно. Давайте разберемся, почему сборщик мусора не смог удалить отработавшие объекты из памяти. Откроем кратчайший граф ссылок на один из Activity:

Борьба с утечками памяти в Android. Часть 1

На скриншоте видно, что на Activity ссылается this$0. Это неявная ссылка из анонимного класса на внешний класс. В Java любой анонимный класс всегда имеет неявную ссылку на внешний класс, даже если вы никогда не обращаетесь к внешним полям или методам. Java не идеальна, а жизнь — это боль. Такие дела, котаны.

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

Итак, после того как мы поместили Runnable или Message в Handler, он будет хранится в списке сообщений в LooperThread до тех пор, пока сообщение не отработает. Вполне очевидно, что если мы поместим отложенное сообщение, то оно будет лежать в памяти до тех пор, пока не настанет его время. Вместе с сообщением в памяти будут лежать все объекты, на которые ссылается сообщение, явно и неявно.
И с этим нужно что-то делать.

Решение с использованием статического класса

Давайте попробуем решить нашу проблему, избавившись от ссылки this$0. Для этого переделаем анонимный класс в статический:

Борьба с утечками памяти в Android. Часть 1

Запускаем, пару раз поворачиваем телефон и собираем дамп памяти.

Борьба с утечками памяти в Android. Часть 1

Снова больше одной Activity? Давайте посмотрим, почему сборщик мусора не смог их удалить.

Борьба с утечками памяти в Android. Часть 1

Обратите внимание на самый низ графа ссылок: Activity сохранен в ссылке mContext из mTextView внутри класса DoneRunnable. Очевидно, что использование статического класса самого по себе недостаточно, чтобы избежать утечки памяти. Нам нужно сделать кое-что еще.

Решение с использованием статического класса и WeakReference

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

Борьба с утечками памяти в Android. Часть 1

Обратите внимание, что мы сохраняем ссылку на TextView в WeakReference. Использование WeakReference требует особой аккуратности: такая ссылка в любой момент может обнулиться. Поэтому сначала сохраняем ссылку в локальную переменную и работаем только с последней, проверив ее на null.

Запускаем, поворачиваем и собираем дамп памяти.

Борьба с утечками памяти в Android. Часть 1

Мы добились желаемого! Только один Activity в памяти. Проблема решена.

Для использования данного подхода нам необходимо:

  • использовать статический внутренний или внешний класс;
  • использовать WeakReference для всех объектов, на которые мы ссылаемся.

Хорош ли данный метод?
Если сравнивать оригинальный код и «безопасный» код, то в глаза бросается большое количество «шума». Он отвлекает от понимания кода и усложняет его поддержку. Написание такого кода — то еще удовольствие, не говоря уж о том, что можно что-то забыть или забить.

Хорошо, что есть решения получше.

Очистка всех сообщений в onDestroy

У класса Handler есть занимательный и очень полезный метод — removeCallbacksAndMessages, который принимает null в качестве аргумента. Он удаляет все сообщения, находящиеся в очереди данного Handler'а. Давайте используем его в onDestroy.

Борьба с утечками памяти в Android. Часть 1

Запустим, повернем и снимем дамп памяти.

Борьба с утечками памяти в Android. Часть 1

Прекрасно! Только один класс Activity.

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

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

Решение с использованием WeakHandler

Команда Badoo написала свой Handler — WeakHandler. Это класс, который ведет себя совершенно как Handler, но исключает утечки памяти.

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

Борьба с утечками памяти в Android. Часть 1

Очень похоже на оригинальный код, не так ли? Лишь одна маленькая деталь: вместо использования android.os.Handler мы использовали WeakHandler. Давайте запустим, повернем телефон несколько раз и снимем дамп памяти.

Борьба с утечками памяти в Android. Часть 1

Наконец-то! Код чист как слеза и память не течет.

Если вам понравился этот метод, то вот хорошая новость: использовать WeakHandler очень просто.

Добавьте maven-зависимость в ваш проект:

repositories {
    maven {
        repositories {
            url 'https://oss.sonatype.org/content/repositories/releases/'
        }
    }
}

dependencies {
    compile 'com.badoo.mobile:android-weak-handler:1.0'
}

Импортируйте WeakHandler в вашем коде:

import com.badoo.mobile.util.WeakHandler

Исходный код выложен на github: github.com/badoo/android-weak-handler.

Принцип работы WeakHandler

Главная идея — держать жесткую ссылку на сообщения или Runnable до тех пор, пока существует жесткая ссылка на WeakHandler. Как только WeakHandler может быть удален из памяти, все остальное должно быть удалено вместе с ним.

Для простоты объяснения мы покажем простенькую диаграмму, демонстрирующую разницу между помещением анонимного Runnable в простой Handler и в WeakHandler:

Борьба с утечками памяти в Android. Часть 1

Обратите внимание на верхнюю диаграмму: Activity ссылается на Handler, который постит Runnable (помещает его в очередь сообщений, на которые ссылается Thread). Все неплохо, за исключением неявной обратной ссылки из Runnable на Activity. Пока Message лежит в очереди, которая живет, пока жив Thread, весь граф не может быть собран сборщиком мусора. В том числе и толстая Activity.

В нижней диаграмме Activity ссылается на WeakHandler, который держит Handler внутри. Когда мы просим его поместить Runnable, он заворачивает его в WeakRunnable и постит в очередь. Таким образом, очередь сообщений ссылается только на WeakRunnable. WeakRunnable содержит WeakReference на изначальный Runnable, т.е. сборщик мусора может его очистить в любой момент. Что бы он его не очистил раньше времени, WeakHandler держит жесткую ссылку на Runnable. Но как только сам WeakHandler может быть удален, Runnable так же может быть удален.

Нужно быть аккуратным и не забывать, что на WeakHandler должна быть ссылка извне, иначе все сообщения будут очищены вместе с ним сборщиком мусора.

Выводы

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

  • использовать статический внутренний класс Runnable/Handler с WeakReferences на внешний класс;
  • чистить все сообщения в классе Handler из метода onDestroy;
  • использовать WeakHandler от Badoo (https://github.com/badoo/android-weak-handler).

Выбор за вами. Первый метод точно не для ленивых. Второй метод выглядит довольно простым, но требует дополнительной работы. Третий же — наш фаворит, но нужно быть внимательным: на WeakHandler должна быть внешняя ссылка до тех пор, пока он вам нужен, иначе сборщик мусора его удалит вместе со всеми сообщениями из очереди.

Удачной вам борьбы! Оставайтесь с нами — у этой темы будет продолжение.

Статья в нашем англоязычном блоге: bit.ly/AndroidHandlerMemoryLeaks

Дмитрий Воронкевич, ведущий разработчик

Автор: mutable

Источник

Поделиться

* - обязательные к заполнению поля