Как сделать утечку памяти с помощью подписок в 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