RxJava 的資源管理

前言

前面介紹了關於 RxSwift 的資源管理,這次要來介紹關於 RxJava 的部分,在 RxJava 中有一些第三方的資源管理像是 RxLifecycle 這種好用的第三方庫來幫忙在程式碼中,如果沒有及時的回收 Rx 相關的資源,會造成 Activity/Fragment 無法銷毀所導致的 Memory LeakRxLifecycle 的用法就是讓我們的 Observable 跟隨著 Activity/Fragment 的生命週期去取消訂閱,但是這樣就真的完美解決了 RxJava 的記憶體管理問題嗎 ? 我們來看看 RxLifecycle 的作者怎麼說。

為什麼不要用 RxLifecycle ?

這是RxLifecycle 的作者 Dan Lew 在 2017年8月所發表的文章 Why Not RxLifecycle,看完這篇文章加上自身專案目前所遇到的問題,只能說太晚作者才發這一篇了QQ,我們來看一下原文怎麼說的。

由來

Dan LewTrello 工作時,他們一開始使用 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 the Adapter wants to send it a new one, so it passes it a new Observable. You want to unsubscribe from the last Observable 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 using unsubscribe() 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 between onComplete() 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 except Observable, 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 or Fragment 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 a CompositeSubscription) then manually calling unsubscribe() / 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 resulting Subscriptionupwards 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 function fun listen(data: Observable<Data>): Subscription. That way the Adapter can listen to the Observable, but is not responsible for knowing when it needs to stop listening; that responsibility is explicitly given to the owner of the Adapter.

    This pattern can be applied repeatedly to as many layers as you want. You could have an Activity that creates a View that contains a RecyclerViewthat creates an Adapter that listens to an Observable… but as long as you pass that Subscription upwards at each layer, it will eventually make its way back to a parent (possibly the Activity 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 call mySubscription?.unsubscribe().

  • A simple CompositeSubscription operator extension lets you use += to add Subscriptions. Otherwise you need to wrap your whole Observable 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()