Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

LeakCanary — канарейка для поиска утечек памяти

Статья с изменениями взята с сайта Хабрахабр. Сама статья является переводом поста автора библиотеки. Обратите внимание, что автор для примера использует класс Кот и класс Коробка. Определённо, он наш человек.

Никому не нравятся ошибки с OutOfMemoryError


java.lang.OutOfMemoryError
        at android.graphics.Bitmap.nativeCreate(Bitmap.java:-2)
        at android.graphics.Bitmap.createBitmap(Bitmap.java:689)
        at com.squareup.ui.SignView.createSignatureBitmap(SignView.java:121)

Работая над Square Register, мы рисовали подпись клиента, используя битмап-кэш. Поскольку этот битмап размером с экран устройства, у нас было очень много ошибок с переполнением памяти OutOfMemory (OOM).

Мы пробовали несколько подходов ни один из которых не решил проблему:

  • Использовали Bitmap.Config.ALPHA_8 (подпись не требовала цвет)
  • Ловили OutOfMemoryError, вызывали сборку мусора и пробовали снова (подглядели в GCUtils)
  • Мы не рассматривали вариант с размещением битмапов вне кучи Java. К счастью Fresco еще не существовало

Мы шли по ложному пути.

Проблема была не в размере битмапа. Когда память почти полностью использована, OOM может возникнуть где угодно. Конечно, вероятность, что это произойдет во время выделения больших объектов, вроде битмапов, увеличивается. OOM — это лишь симптом более глубокой проблемы: утечек памяти.

Что такое утечка памяти?

Многие объекты имеют ограниченное время жизни. Когда их время жизни заканчивается, ожидаются, что они будут собраны сборщиком мусора. Если цепочка ссылок указывает на объект в памяти, после ожидаемого завершения времени жизни объекта, то это создаёт утечку памяти. При накоплении некоторого количества утечек, у программы начинает заканчивается память.

Например, после того как был вызван метод Activity.onDestroy, то активность и вся иерархия компонентов и связанные с ними битмапы должны быть доступны для сборки мусора. Если фоновый поток во время работы удерживает ссылку на активность, то занимаемая ей память не может быть освобождена. Рано или поздно это приведет к выбросу ошибки OutOfMemoryError.

Охота на утечки

Охота на утечки памяти - это ручной процесс, хорошо описанный в серии статей Wrangling Dalvik от Raizlabs.

Вот ключевые этапы:

  • Узнать об ошибках OutOfMemoryError через Bugsnag, Crashlytics или консоль разработчика
  • Попытаться воспроизвести проблему. Возможно вам понадобится купить, одолжить или украсть конкретное устройство, подверженное ошибке (Не на всех девайсах проявляются все утечки!). Также необходимо восстановить последовательность действий приводящих к утечке, возможно грубым перебором.
  • Сделать дамп памяти при OutOfMemoryError (Здесь можно узнать как)
  • Изучить дамп памяти с помощью MAT или YourKit и обнаружить объекты, которые должны были быть собраны сборщиком мусора
  • Найти самые короткие пути ссыльных ссылок от объекта до корней сборщика мусора (GC Roots)
  • Найти ссылку, которой не должно быть, и исправить утечку памяти

Что, если существовала бы библиотека, которая могла сделать это всё, ещё до возникновения OOM и позволила бы сосредоточится на исправлении утечки памяти?

Представляем LeakCanary

LeakCanary — это Java-библиотека с открытым исходным кодом для обнаружения утечек памяти в отладочных сборках.

Давайте взглянем на следующий пример:


class Cat {
}
class Box {
  Cat hiddenCat;
}
class Docker {
  static Box container;
}

// ...

Box box = new Box();
Cat schrodingerCat = new Cat();
box.hiddenCat = schrodingerCat;
Docker.container = box;

Затем вы создаёте объект RefWatcher и предоставляете ему объект для отслеживания:


// Мы ожидаем что schrodingerCat должен исчезнуть (или нет). Давайте отслеживать его.
refWatcher.watch(schrodingerCat);

После того, как утечка была обнаружена, вы получаете неплохой трейс утечки.


* GC ROOT static Docker.container
* references Box.hiddenCat
* leaks Cat instance

Установка очень проста. С помощью всего одной строчки кода LeakCanary автоматически будет обнаруживать утечки активности:


public class ExampleApplication extends Application {
    @Override public void onCreate() {
        super.onCreate();
        LeakCanary.install(this);
    }
}

Вы получаете уведомление в неплохом виде:

Заключение

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

Результаты потрясающие: на 94% меньше ошибок с OutOfMemoryError!

Установи LeakCanary сейчас!

Установка

В файле build.gradle:


// новая ветка 2
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'

// старая ветка 1.6
dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
  // Optional, if you use support library fragments:
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.2'
}

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

В классе Application:


public class ExampleApplication extends Application {

    @Override public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
          // This process is dedicated to LeakCanary for heap analysis.
          // You should not init your app in this process.
          return;
        }
        LeakCanary.install(this);
        // Normal app init code...
    }
}

Готово! LeakCanary будет автоматически показывать уведомления при обнаружении утечки памяти.

LeakCanary.install() возвращает предварительно сконфигурированные RefWatcher. А также устанавливает ActivityRefWatcher, который автоматически определяет утечку после Activity.onDestroy().


public class ExampleApplication extends Application {

  public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context.getApplicationContext();
    return application.refWatcher;
  }

  private RefWatcher refWatcher;

  @Override public void onCreate() {
    super.onCreate();
    refWatcher = LeakCanary.install(this);
  }
}

Можно использовать RefWatcher для отслеживания утечек памяти у фрагментов:


public abstract class BaseFragment extends Fragment {

    @Override public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
        refWatcher.watch(this);
    }
}

Новая ветка 2.х

Вышла новая версия:

  • новая значок
  • написан на Kotlin
  • хранение данных в БД, а не в файлах
  • ищет несколько утечек в одном дампе
  • группирует похожие утечки
  • больше детализации
  • не нужен Application.onCreate

Видео

Дополнительное чтение

LeakCanary 1.6 - новинки в версии 1.6.

Реклама