Как да изтече памет с абонаменти в RxJava

Има много страхотни статии за практиката за RxJava. Той опростява нещата значително при работа с Android Framework, но бъдете внимателни, защото опростяването може да има свои клопки. В следващите части ще проучите една от тях и ще видите колко лесно е да създадете теч на памет с абонаменти на RxJava.

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

Представете си, че вашият мениджър се обади и ви помоли да създадете джаджа, която показва произволно заглавие на филма. Тя трябва да се основава на някаква служба за външни препоръки. Тази джаджа трябва да показва заглавие на филма по желание на потребителя или може да го направи самостоятелно. Мениджърът също така иска тази джаджа да може да съхранява някаква информация, свързана с взаимодействието на потребителя с нея.

Подходът, базиран на MVP, е един от многото начини за това. Създавате опростен изглед, съдържащ джаджите както ProrogressBar, така иTextView. Рекомендуемото MoovieUseCase дръжки предоставя случайно заглавие на филма.
Presenter се свързва с случай на използване и показва заглавие на изглед. Запазването на състоянието на презентатора се осъществява, като се запаметява в паметта, дори когато вашата активност се пресъздава (в така наречения NonConfigurationScope).

Ето как изглежда Вашият Presenter. За целите на тази статия, нека приемем, че искате да запазите флаг, указващ дали потребителят е натиснал заглавието.

Приспособлението ще бъде добавено към лилав контейнер, когато потребителят поиска препоръка. Той ще бъде премахнат, след като потребителят реши да го изчисти.

Минимално жизнеспособен продукт, а?

Изглежда всичко засега работи добре.

За да бъдем по-сигурни, решихме да инициализираме StrictMode в изграждането на грешки.
Започваме да играем с нашето приложение и се опитваме да завъртим устройството си няколко пъти. Изведнъж се появява лог съобщение.

StrictMode ни удря с приятелско предупреждение

Това не звучи правилно. Можете да опитате да изхвърлите текущото състояние на паметта и да разгледате по-задълбочено проблема:

Изхвърляне на паметта след предупреждението на StrictMode

Там е вашият виновник, маркиран със синия шрифт. По някаква причина все още има екземпляр на MovieSuggestionView, който съдържа препратка към стар екземпляр на MainActivity.

Но защо? Отписахте се от основната си работа и също така изчистихте препратката към MovieSuggestionView, когато изпускате изгледа от своя презентатор. Откъде идва този теч?

Търсене на теч

Съхранявайки препратка към абонамент, вие всъщност съхранявате екземпляр на ActionSubscriber , който изглежда така:

Тъй като полетата onNext, onError и onCompleted са окончателни, няма чист начин за тяхното обезсилване. Проблемът е, че извикването на абонамент () на Абонат само го маркира като отписано (и няколко неща повече, но в нашия случай не е важно).

За тези, които се чудят откъде идва този обект ActionSubscriber, погледнете дефиницията на метода на абониране:

По-нататъшният анализ на сметището показва, че референцията на MovieSuggestionView наистина все още се съхранява вътре в полетата onNext и onError.

Прегледът все още се позовава от ActionSubscriber

За да разберем по-добре проблема, нека копаем малко по-дълбоко и да видим какво ще стане след като се компилира кодът ви.

=> ls -1 app / build / междинни продукти / класове / debug / me / scana / subscriptionsleak
...
Водещ $ 1.class
Водещ $ 2.class
Presenter.class
...

Можете да видите, че в допълнение към основния ви клас Presenter, получавате два допълнителни файла от клас, по един за всеки от анонимните класове Action1 <>, които сте въвели.

Нека проверим какво се случва вътре в един от тези анонимни класове с помощта на удобен инструмент за javap:

=> javap -c презентатор \ $ 1

Може би сте чували, че анонимен клас съдържа неявна препратка към външен клас. Оказва се, че анонимните класове също улавят всички променливи, които използвате вътре в тях.

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

Знаете какво не е наред с настоящото ни решение. И така, как да го поправите?

Това е доста лесно.

Можете да зададете нашия обект за абонамент на Subscription.empty (), като по този начин изчистите препратка към стар ActionObserver.

Има и клас CompositeSubscription, който позволява да се съхраняват множество обекти на абонамента и да се отпише () върху тях. Това трябва да ни освободи да съхраняваме директно справка за абонамент. Имайте предвид обаче, че това все още няма да реши проблема ви. Референциите все още ще се съхраняват в CompositeSubscription.

За щастие, има ясен () метод, който отписва всичко и след това изчиства препратките. Също така ви позволява да използвате повторно обект CompositeSubscription, за разлика от отписването (), което прави вашия обект неизползваем.

Тук е фиксиран клас Presenter с един от горепосочените методи:

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

За да обобщим нещата:

  • Обектите за абонамент съдържат окончателни препратки към вашите повиквания. Обратните ви повиквания могат да съдържат препратки към обекти, свързани с жизнения цикъл на вашия Android. И двамата могат да изтекат паметта, когато не се третират внимателно
  • Можете да използвате инструменти като StrictMode, javap, HPROF Viewer, за да намерите и анализирате източника на течове. Не го споменах в статията, но можете да разгледате и библиотеката на LeakCanary от Square.
  • Копаенето в библиотеки, които използвате ежедневно, помага много за решаването на потенциални проблеми, които могат да възникнат

Благодаря!

Надявам се, че сте харесали статията и може би дори сте научили нещо
ново от него.
Моля, не се колебайте да зададете въпрос и да споделите собствените си идеи за работа с RxJava в секцията за коментари по-долу!
Можете също да се свържете с мен в акаунта ми в Twitter :)

Може да ви се сторят и интересни:

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

Проблем за крайните справки в абонатите на GitHub:
https://github.com/ReactiveX/RxJava/issues/3148

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

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

javap, Разглобяващ файл на Java клас
http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javap.html