RxJava記憶體洩漏的一種解決方案

weixin_34208283發表於2017-11-13

RxJava大家應該都用過或者聽過,在用RxJava的時候當被觀察者(Observable)和觀察者(Observer)產生訂閱關係後沒有及時釋放這種subscription就很容易產生記憶體洩漏,一個典型的場景就是使用RxJava發起網路請求,此時應用程式被殺掉,這種訂閱關係就沒有得到及時釋放。當然這種情況在onDestroy中手動進行判斷也行。如果是這種場景,發起的網路請求還沒成功返回,此時應用進入後臺,這時候就算請求成功返回也不應該更新UI,這種情況在使用RxJava的情況下怎麼處理?

我們遇到的情況早有大神提供瞭解決方案,就是RxLifecycle開源庫,官方的定義:

RxLifecycle

This library allows one to automatically complete sequences based on a second lifecycle stream.

This capability is useful in Android, where incomplete subscriptions can cause memory leaks.

簡單來講就是可以用來處理RxJava產生的記憶體洩漏。今天我們主要看下RxLifecycle的幾種使用方式。後面有機會再分析下原始碼。

主要有下面幾種使用方式:

1 bindToLifecycle
2 bindUntilEvent
3 LifecycleProvider

首先需要在模組的build.gradle中新增依賴:

compile 'com.trello.rxlifecycle2:rxlifecycle-components:2.1.0'
compile 'com.trello.rxlifecycle2:rxlifecycle-navi:2.1.0'

1.bindToLifecycle

這種方式可以自動根據Activity或者Fragment的生命週期進行解綁,用起來也很方便,Avtivity需要繼承RxActivity, Fragment則需要繼承RxFragment

public class MainActivity extends BaseActivity{
    @Override
    protected void onStart() {
        super.onStart();
        Observable.interval(1, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .compose(this.<Long>bindToLifecycle())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long num) throws Exception {
                        Timber.tag(TAG).d("onStart, running num : " + num);
                    }
                });
    }
}

這裡在onStart中進行繫結,如果Activity進入onStop生命週期的時候就會停止Observable,看一下日誌:

2608779-e17923f58d48d4bc.PNG
bindToLifecycle.PNG

bindToLifecycle就是會自動繫結生命週期,我們看下Activity生命週期,很明顯在onCreate中bindToLifecycle就會在onDestroy中進行解綁,其他的一一對應就是。

/**
 * Lifecycle events that can be emitted by Activities.
 */
public enum ActivityEvent {

    CREATE,
    START,
    RESUME,
    PAUSE,
    STOP,
    DESTROY

}

Fragment也有對應的生命週期,也是對稱對應的。

/**
 * Lifecycle events that can be emitted by Fragments.
 */
public enum FragmentEvent {

    ATTACH,
    CREATE,
    CREATE_VIEW,
    START,
    RESUME,
    PAUSE,
    STOP,
    DESTROY_VIEW,
    DESTROY,
    DETACH

}

2.bindUntilEvent

見名知意,就是可以和制定的生命週期進行繫結,這種方式比上面的靈活,比如可以在一個按鈕中繫結onStart的事件,而不必要一定要解除安裝onStart中。

我在上面的Activity中新增一個按鈕,點選事件在getData(View view)中,看下程式碼:

public void getData(View view) {
    Observable observable = Observable.interval(1, TimeUnit.SECONDS).
            subscribeOn(Schedulers.io()).compose(this.bindUntilEvent(ActivityEvent.PAUSE));
    observable.observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<Long>() {
                @Override
                public void accept(Long num) throws Exception {
                    Timber.tag(TAG).d("getData, running until num : " + num);
                }
            });
}

按我們的預期,就是在Activity進入onPause時,Observable會停止傳送資料,看下列印的日誌是不是這樣的:


2608779-8a75a72f5877406e.PNG
bindUntilEvent.PNG

基本上面這兩種方式就夠用了,下面的方式LifecycleProvider在MVP的模式中用處就比較大了。我們接著往下看。

3.LifecycleProvider

使用方式就是,首先繼承NaviActivity,然後在Activity中加上這句話

LifecycleProvider<ActivityEvent> provider = NaviLifecycle.createActivityLifecycleProvider(this);

這樣就可以通過provider監聽生命週期了。我這裡在初始化presenter的時候傳遞過去

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ......
        //初始化Presenter
        presenter = new Presenter(provider);
    }

在Activity中新增一個按鈕,通過延時3秒模擬網路請求,請求成功後更新UI。這裡通過 provider傳送生命週期事件,然後在onNext中判斷事件型別,如果已經Activity已經進入onPause onStop onDestroy中的其中一個,就不再更新UI了,並且通過Disposable斷開連線。

FactoryModel.getModel(Token.STRING_MODEL).params(params).execute(new CallBack<String>() {
            @Override
            public void onSuccess(final String data) {
                provider.lifecycle().subscribe(new Observer<ActivityEvent>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        disposable = d;
                    }

                    @Override
                    public void onNext(ActivityEvent activityEvent) {
                        Timber.tag("Presenter_TAG").i("received activityEvent, activityEvent = %s" , activityEvent.name());
                        if (null != disposable && disposable.isDisposed()){
                            Timber.tag("Presenter_TAG").i("disposable isDisposed");
                            return;
                        }
                        if (activityEvent == ActivityEvent.PAUSE || activityEvent == ActivityEvent.STOP || activityEvent == ActivityEvent.DESTROY){
                            Timber.tag("Presenter_TAG").e("do not refresh UI, activityEvent = %s", activityEvent.name());
                            onComplete();
                            return;
                        }
                        if (isViewAttached()) {
                            Timber.tag("Presenter_TAG").i("refresh UI, activityEvent = %s" , activityEvent.name());
                            view.showData(data);
                        }
                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onComplete() {
                        if (null != disposable && !disposable.isDisposed()){
                            disposable.dispose();
                            Timber.tag("Presenter_TAG").d("LifecycleProvider disposed");
                        }
                    }
                });
            }
        });

看一下正常請求成功的日誌,和預期一樣,正常更新UI了。


2608779-d692e821bc0a732c.PNG
請求成功.PNG

下面我們來搞一下破壞,在剛發起網路請求後,按home按鍵將APP切到後臺,這樣Activity就進入onStop的生命週期,那麼即使網路請求成功也應該再更新UI了,看下日誌是不是這樣的:

2608779-b8004a54b36a759e.PNG
破壞.PNG

最終UI也是沒有更新的。我們在下面的邏輯中切斷了訂閱關係,那麼下次再次點選按鈕發起網路請求是還能正常使用provider嗎?

if (activityEvent == ActivityEvent.PAUSE || activityEvent == ActivityEvent.STOP || activityEvent == ActivityEvent.DESTROY){
     Timber.tag("Presenter_TAG").e("do not refresh UI, activityEvent = %s", activityEvent.name());
     onComplete();
     return;
}

其實是可以的,看一下Demo的日誌:

2608779-e66d159b7d737934.PNG
切斷訂閱關係後再次監聽生命週期.PNG

那麼是怎麼回事?其實就在下面這個程式碼中:

provider.lifecycle()

在獲取Observable時會清楚原先的特徵,我們看下原始碼:

private final BehaviorSubject<ActivityEvent> lifecycleSubject = BehaviorSubject.create();

@Override
@NonNull
@CheckResult
public Observable<ActivityEvent> lifecycle() {
    return lifecycleSubject.hide();
}

再跟進去BehaviorSubject,其中以後一句註釋Hides the identity of this Observable and its Disposable,最後會返回一個新的Observable :

/**
     * Hides the identity of this Observable and its Disposable.
     * <p>Allows hiding extra features such as {@link io.reactivex.subjects.Subject}'s
     * {@link Observer} methods or preventing certain identity-based
     * optimizations (fusion).
     * <dl>
     *  <dt><b>Scheduler:</b></dt>
     *  <dd>{@code hide} does not operate by default on a particular {@link Scheduler}.</dd>
     * </dl>
     * @return the new Observable instance
     *
     * @since 2.0
     */
    @CheckReturnValue
    @SchedulerSupport(SchedulerSupport.NONE)
    public final Observable<T> hide() {
        return RxJavaPlugins.onAssembly(new ObservableHide<T>(this));
    }

4.總結

RxLifecycle的侵入性還是比較低的,基本不需要改動原來的程式碼就可以實現生命週期的監聽,也提供了防止RxJava訂閱關係記憶體洩漏的另外一種解決方案,還是很不錯的。

今天的分享就到這了,後面有機會和大家分享下RxLifecycle的原始碼。

平時工作也比較忙,寫部落格真的是需要耐力,如果對大家有幫助歡迎關注和點贊哈。

最後感謝@右傾傾的理解和支援哈。

以上!

相關文章