RxJava 的資源管理
前言
前面介紹了關於 RxSwift 的資源管理,這次要來介紹關於
RxJava
的部分,在RxJava
中有一些第三方的資源管理像是 RxLifecycle 這種好用的第三方庫來幫忙在程式碼中,如果沒有及時的回收 Rx 相關的資源,會造成Activity/Fragment
無法銷毀所導致的Memory Leak
,RxLifecycle
的用法就是讓我們的Observable
跟隨著Activity/Fragment
的生命週期去取消訂閱,但是這樣就真的完美解決了RxJava
的記憶體管理問題嗎 ? 我們來看看RxLifecycle
的作者怎麼說。
為什麼不要用 RxLifecycle ?
這是RxLifecycle
的作者 Dan Lew 在 2017年8月所發表的文章 Why Not RxLifecycle,看完這篇文章加上自身專案目前所遇到的問題,只能說太晚作者才發這一篇了QQ,我們來看一下原文怎麼說的。
由來
當 Dan Lew
在 Trello
工作時,他們一開始使用 RxJava
也遇到了 Memeory leak
相關的問題, 當你設定任何一個 Subscriptions
似乎都會造成 Memory Leak
,除非你明確定義的清除它,所以在使用它們的時候,不斷的重複訂閱與取消訂閱。
手動處理取消訂閱是相當乏味的,在大多數情況下,我們希望我們的 Fragment
或者 Activity
生命週期結束時,我們所有的 Subscriptions
也跟著結束,因次 RxLifecycle
誕生了,使用 RxLifecycle
你只需要呼叫 compose()
並綁定到某些生命週期時發生。那麼看似完美的就不需要在乎 Memory Leak
了。
問題
但是這樣綁定 Activity/Fragment
的生命週期這樣子的設計,隨著時間的發展,他出現越來越多致命的缺陷(在我們專案開發中,同樣有這樣子的感受)。
由於自動檢測生命週期,導致代碼混淆,假設你在
onStart()
中訂閱,那麼以RxLifecycle
的機制,將會自動幫你在onStop()
取消訂閱,這樣子並不是什麼大問題,但是,如果你在一個非Activity
組件內部時呢?你必須讓你的這個組件擁有Activity
的生命週期,然後希望它在生命週期中的正確的時機點訂閱,但是這不是保證的,更糟糕的是,當訂閱失敗時,通常是模糊的。
舉例來說。 假設你有一個Adapter
它需要訂閱Observable
和(在某個時機) 取消訂閱。RxLifecycle
的關鍵問題是: 您如何知道自動取消訂閱在適當的時機發生 ? 當然我們可以使用更明確的bindUntilEvent()
去避免這樣的自動訂閱問題,但是卻減少了RxLifecycle
的實用性。**Often times you end up manually handling the Subscription anyways.**Let’s extend the
Adapter
example above. You’re listening to one data source, but then whoever is controlling theAdapter
wants to send it a new one, so it passes it a newObservable
. You want to unsubscribe from the lastObservable
before subscribing to the new one. None of this has anything to do with the lifecycle, and thus must be handled manually.Having to manually handle
Subscriptions
anyways means that RxLifecycle is just an extra headache. It’s confusing to developers - why are we usingunsubscribe()
in one place and RxLifecycle in another?RxLifecycle can only simulate Subscription.unsubscribe(). Because of RxJava 1 limitations, it can (at most) simulate the stream ending due to
onComplete()
. 99% of the time this is fine, but it leaves open the door for developer mistakes due to subtle differences betweenonComplete()
vs unsubscription.RxLifecycle throws exceptions for Single / Completable. Again, because we can only simulate the stream ending.
Single
/Completable
either emit or error, so there’s no other choice. For a while we weren’t using anything exceptObservable
, but now that we’re using other types this can cause problems.Subtle timing bugs require calling RxLifecycle late in the stream. It’s an avoidable issue, but again can lead to developer mistakes that are best avoided.
RxLint cannot detect when you’re using RxLifecycle bindings. RxLint is a handy tool and using RxLifecycle lessens its utility.
It generally requires subclassing Activity / Fragment. While not a requirement (since it’s implemented using interfaces), not subclassing leads to a lot of busywork reproducing what the library does. That’s fine most of the time, but every once in a while we need to use a specialized
Activity
orFragment
and that causes pain.
(Note that this minor problem can soon be fixed via Google’s lifecycle-aware components.)
以上這些問題都歸咎到了 RxLifecycle
的自動性質可能會產生複雜且意想不到的副作用以及後果,這樣違反了當初 RxLifecycle
被建立出來的本意。
更好的解決方式
這邊是作者最後開始做的改善,而不使用自己的 RxLifecycle
了。或許我們也應該去思考除了作者提出的解決方法,自己是不是能有一些更好的方式去處理我們的記憶體及 Subscriptions
管理,而不依賴 RxLifecycle
Manually manage Subscriptions. That means hanging onto
Subscriptions
(or stuffing them into aCompositeSubscription
) then manually callingunsubscribe()
/clear()
when appropriate.Now that I’m used to the idea it’s not so bad. Its explicit nature makes code easier to reason about. It doesn’t require me to think through a complex flow of logic or anticipate unexpected consequences. The extra boilerplate is worth the simplicity.
**Components pass their Subscriptions upwards until someone handles it.**In other words, if a component is given an
Observable
from its parent but does not know when to unsubscribe, it passes the resultingSubscription
upwards to the parent, since the parent should have a better grasp of the lifecycle.Let’s look at that
Adapter
example from before. We now provide a functionfun listen(data: Observable<Data>): Subscription
. That way theAdapter
can listen to theObservable
, but is not responsible for knowing when it needs to stop listening; that responsibility is explicitly given to the owner of theAdapter
.This pattern can be applied repeatedly to as many layers as you want. You could have an
Activity
that creates a View that contains aRecyclerView
that creates anAdapter
that listens to anObservable
… but as long as you pass that Subscription upwards at each layer, it will eventually make its way back to a parent (possibly theActivity
itself) who knows when to unsubscribe.
Another subtle reason for the switch away from RxLifecycle is our adoption of Kotlin. Kotlin makes manually handling Subscriptions easier for two reasons:
Unsubscribing from nullable
Subscriptions
is a simple one-liner. Before you had to check for nullability (or use a one-liner utility function). Annoying. Now you can just callmySubscription?.unsubscribe()
.A simple
CompositeSubscription
operator extension lets you use+=
to addSubscriptions
. Otherwise you need to wrap your wholeObservable
chain in parentheses, which is a huge pain formatting-wise.Here’s the extension in all its glory:
operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(subscription)
As a result, you can simply use a CompositeSubscription like so:
compositeSubscription += Observable.just().etc().subscribe()