Как сделать утечку памяти с помощью подписок в RxJava

Берегись!

Эта статья устарела, так как она охватывает и старую версию RxJava - вы можете найти обновленную версию для RxJava2 здесь: https://medium.com/@scanarch/how-to-leak-memory- с-одноразовыми-в-rxjava2-715dd80bf966

— — — — — — — —

Есть много отличных статей о RxJava. Это значительно упрощает работу с платформой Android, но будьте осторожны, потому что упрощение может иметь свои подводные камни. В следующих частях вы собираетесь изучить один из них и увидеть, как легко создать утечку памяти с помощью Subscriptions RxJava.

Решение простой задачи

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

Подход на основе MVP - один из многих способов сделать это. Вы создаете простое представление, содержащее обаProgressBar иTextView виджета. RecommendedMovieUseCase обрабатывает случайное название фильма.
Presenter подключается к варианту использования и отображает заголовок в представлении. Сохранение состояния докладчика осуществляется путем сохранения его в памяти, даже когда ваше действие воссоздается (в так называемом NonConfigurationScope).

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

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

Вроде пока все работает нормально.

Для большей безопасности мы решили инициализировать StrictMode в отладочных сборках.
Мы начинаем экспериментировать с нашим приложением и пытаемся несколько раз повернуть наше устройство. Внезапно появляется сообщение журнала.

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

Синим шрифтом отмечен ваш виновник. По какой-то причине все еще существует экземпляр MovieSuggestionView, содержащий ссылку на старый MainActivity экземпляр.

Но почему? Вы отказались от подписки на фоновое задание, а также удалили ссылку на MovieSuggestionView при удалении представления из своего Presenter. Откуда эта утечка?

Поиск утечки

Сохраняя ссылку на Subscription, вы фактически сохраняете экземпляр ActionSubscriber<T>, который выглядит следующим образом:

Поскольку поля onNext, onError и onCompleted являются окончательными, нет чистого способа их обнулить. Проблема в том, что вызов unsubscribe() на Subscriber только отмечает его как отписанный (и еще кое-что, но в нашем случае это не важно).

Для тех, кому интересно, откуда взялся этот ActionSubscriber объект, взгляните на определение метода subscribe:

Дальнейший анализ дампа памяти доказывает, что ссылка MovieSuggestionView действительно все еще хранится внутри полей onNext и onError.

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

=> ls -1 app/build/intermediates/classes/debug/me/scana/subscriptionsleak
...
Presenter$1.class
Presenter$2.class
Presenter.class
...

Вы можете видеть, что в дополнение к вашему основному Presenter классу вы получаете два дополнительных файла классов, по одному для каждого из анонимных Action1<> классов, которые вы ввели.

Давайте посмотрим, что происходит внутри одного из этих анонимных классов, с помощью удобного инструмента javap:

=> javap -c Presenter\$1

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

Из-за этого, сохраняя ссылку на объект Subscription, вы эффективно сохраняете ссылки на те анонимные классы, которые вы использовали для обработки результата заголовка фильма. Они хранят ссылку на представление, с которым вы хотели что-то сделать, и это ваша утечка.

Вы знаете, что не так с нашим текущим решением. Итак, как это исправить?

Это очень просто.

Вы можете установить для нашего объекта Subscription значение Subscription.empty(), таким образом убрав ссылку на старый ActionObserver.

Существует также класс CompositeSubscription, который позволяет хранить несколько Subscription объектов и выполняет unsubscribe() над ними. Это должно избавить нас от прямого хранения Subscription ссылки. Однако имейте в виду, что это еще не решит вашу проблему. Ссылки по-прежнему будут храниться в CompositeSubscription.

К счастью, есть clear() метод, который отменяет все подписки, а затем очищает ссылки. Это также позволяет вам повторно использовать CompositeSubscription объект, а не unsubscribe(), который делает ваш объект непригодным для использования.

Вот фиксированный класс Presenter, в котором реализован один из вышеупомянутых методов:

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

Подводя итог:

  • Subscription объектов содержат окончательные ссылки на ваши обратные вызовы. Ваши обратные вызовы могут содержать ссылки на объекты вашего Android, связанные с жизненным циклом. Они оба могут привести к утечке памяти при неосторожном обращении
  • Вы можете использовать такие инструменты, как StrictMode, javap, HPROF Viewer, чтобы найти и проанализировать источник утечек. Я не упоминал об этом в статье, но вы также можете проверить библиотеку LeakCanary на Square.
  • Копание в библиотеках, которые вы используете ежедневно, очень помогает в решении потенциальных проблем, которые могут возникнуть.

Спасибо!

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

Вы также можете связаться со мной в моем Твиттере :)

Вам также могут быть интересны:

Возьмите код из этой статьи:
https://github.com/scana/subscriptions-leak-example

Проблема с финальными ссылками в подписчиках на GitHub:
https://github.com/ReactiveX/RxJava/issues/3148

Реализация NonConfigurationScope:
https://github.com/partition/Dagger-Non-Configuration-Scope

StrictMode Android: https://developer.android.com/reference/android/os/StrictMode.html

javap, Дизассемблер файлов классов Java
http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javap.html