RxJava2 實戰知識梳理(10) 螢幕旋轉導致 Activity 重建時恢復任務

澤毛發表於2017-12-21

一、前言

如果我們在AndroidManifest.xml中宣告Activity時,沒有對android:configChanges進行特殊的宣告,那麼在螢幕旋轉時,會導致Activity的重建,幾個關鍵宣告週期的呼叫情況如下所示:

RxJava2 實戰知識梳理(10)   螢幕旋轉導致 Activity 重建時恢復任務
旋轉螢幕前的Activity中的變數都會被銷燬,但是有時候我們某些任務的執行不和Activity的生命週期繫結,這時候我們就可以利用Fragment提供的setRetainInstance方法,該方法的說明如下:
setRetainInstance 方法說明
如果給Fragment設定了該標誌位,那麼在螢幕旋轉之後,雖然它依附的Activity被銷燬了,但是該Fragment的例項會被保留,並且在Activity的銷燬過程中,只會呼叫該FragmentonDetach方法,而不會呼叫onDestroy方法。

而在Activity重建時,會呼叫該Fragment例項的onAttachonActivityCreated方法,但不會呼叫onCreate方法。

根據Fragment提供的這一特性,那麼我們就可以將一些在螢幕旋轉過程中,仍然需要執行的任務放在具有該屬性的Fragment中執行。在 Handling Configuration Changes with Fragments 這篇文章中,作者介紹了通過這個技巧來實現了一個不被中斷的AsyncTask,大家有需要了解詳細說明的可以檢視這篇文章。

今天,我們跟著前人腳步,用RxJava來演示在螢幕旋轉導致Activity重建時,仍然保持後臺任務繼續執行的例子。

二、示例

2.1 示例

首先,我們宣告一個介面,用於FragmentActivity一個ConnectableObservable,使得Activity可以監聽到Fragment中後臺任務的工作進度。

public interface IHolder {
    public void onWorkerPrepared(ConnectableObservable<Long> workerFlow);
}
複製程式碼

下面,我們來實現WorkerFragment,我們在onCreate中建立了資料來源,它每隔1s向下遊傳送資料,在onResume中,通過前面定義的介面向Activity傳遞一個ConnectableObservable用於監聽。這裡最關鍵的是需要呼叫我們前面說到的setRetainInstance方法,最後別忘了,在onDetach中將mHolder置為空,否則它就會持有需要被重建的Activity示例,從而導致記憶體洩漏。

public class WorkerFragment extends Fragment {

    public static final String TAG = WorkerFragment.class.getName();

    private ConnectableObservable<String> mWorker;
    private Disposable mDisposable;
    private IHolder mHolder;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof IHolder) {
            mHolder = (IHolder) context;
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        if (mWorker != null) {
            return;
        }
        Bundle bundle = getArguments();
        final String taskName = (bundle != null ? bundle.getString("task_name") : null);
        mWorker = Observable.create(new ObservableOnSubscribe<String>() {

            @Override
            public void subscribe(ObservableEmitter<String> observableEmitter) throws Exception {

                for (int i = 0; i < 10; i++) {
                    String message = "任務名稱=" + taskName + ", 任務進度=" + i * 10 + "%";
                    try {
                        Log.d(TAG, message);
                        Thread.sleep(1000);
                        //如果已經拋棄,那麼不再繼續任務。
                        if (observableEmitter.isDisposed()) {
                            break;
                        }
                    } catch (InterruptedException error) {
                        if (!observableEmitter.isDisposed()) {
                            observableEmitter.onError(error);
                        }
                    }
                    observableEmitter.onNext(message);
                }
                observableEmitter.onComplete();
            }

        }).subscribeOn(Schedulers.io()).publish();
        mDisposable = mWorker.connect();
    }

    @Override
    public void onResume() {
        super.onResume();
        if (mHolder != null) {
            mHolder.onWorkerPrepared(mWorker);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mDisposable.dispose();
        Log.d(TAG, "onDestroy");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mHolder = null;
    }
}
複製程式碼

最後來看Activity,當點選“開始工作任務”後,我們嘗試新增WorkerFragment,這時候就會走到WorkerFragmentonCreate方法中啟動任務,之後當WorkerFragment走到onResume方法後,就呼叫onWorkerPrepared,讓Activity進行訂閱,Activity就可以收到當前任務進度的通知,來更新UI。而在任務執行完畢之後,我們就可以將該Fragment移除了。

public class RotationPersistActivity extends AppCompatActivity implements IHolder {

    private static final String TAG = RotationPersistActivity.class.getName();

    private Button mBtWorker;
    private TextView mTvResult;
    private CompositeDisposable mCompositeDisposable;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
        setContentView(R.layout.activity_rotation_persist);
        mBtWorker = (Button) findViewById(R.id.bt_start_worker);
        mBtWorker.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                startWorker();
            }

        });
        mTvResult = (TextView) findViewById(R.id.tv_worker_result);
        mCompositeDisposable = new CompositeDisposable();
    }

    @Override
    public void onWorkerPrepared(Observable<String> worker) {
        DisposableObserver<String> disposableObserver = new DisposableObserver<String>() {

            @Override
            public void onNext(String message) {
                mTvResult.setText(message);
            }

            @Override
            public void onError(Throwable throwable) {
                onWorkerFinished();
                mTvResult.setText("任務錯誤");
            }

            @Override
            public void onComplete() {
                onWorkerFinished();
                mTvResult.setText("任務完成");
            }

        };
        worker.observeOn(AndroidSchedulers.mainThread()).subscribe(disposableObserver);
        mCompositeDisposable.add(disposableObserver);
    }

    private void startWorker() {
        WorkerFragment worker = getWorkerFragment();
        if (worker == null) {
            addWorkerFragment();
        } else {
            Log.d(TAG, "WorkerFragment has attach");
        }
    }

    private void onWorkerFinished() {
        Log.d(TAG, "onWorkerFinished");
        removeWorkerFragment();
    }

    private void addWorkerFragment() {
        WorkerFragment workerFragment = new WorkerFragment();
        Bundle bundle = new Bundle();
        bundle.putString("task_name", "學習RxJava2");
        workerFragment.setArguments(bundle);
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();
        transaction.add(workerFragment, WorkerFragment.TAG);
        transaction.commit();
    }

    private void removeWorkerFragment() {
        WorkerFragment workerFragment = getWorkerFragment();
        if (workerFragment != null) {
            FragmentManager manager = getSupportFragmentManager();
            FragmentTransaction transaction = manager.beginTransaction();
            transaction.remove(workerFragment);
            transaction.commit();
        }
    }

    private WorkerFragment getWorkerFragment() {
        FragmentManager manager = getSupportFragmentManager();
        return (WorkerFragment) manager.findFragmentByTag(WorkerFragment.TAG);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        mCompositeDisposable.clear();
    }
}
複製程式碼

我們來演示一下螢幕旋轉時的效果,在螢幕旋轉之後,我們仍然可以繼續收到任務進度的更新:

RxJava2 實戰知識梳理(10)   螢幕旋轉導致 Activity 重建時恢復任務

2.2 示例解析

下面,我們來解釋一下示例中的幾個要點:

2.2.1 Activity 和 Fragment 之間的資料傳遞

資料傳遞分為兩個方向,它們各自可以通過以下方法來實現:

  • ActivityFragment傳遞資料(示例中我們傳遞了任務的名稱) 一般用於向WorkerFragment傳遞一些任務引數,此時可以通過FragmentsetArguments傳入相關的欄位,FragmentonCreate方法中通過getArguments獲取引數。
  • FragmentActivity傳遞資料(示例中我們傳遞了ObservableActivity訂閱以獲取進度) 可以讓Activity實現一個介面,我們在FragmentonAttach方法中獲取Activity例項轉換成對應的介面型別,之後通過它來呼叫Activity的方法,需要注意的是,在Fragment#onDetach時,要將該Activity的引用置空,否則會出現記憶體洩漏。

2.2 為什麼呼叫 publish 方法,使用 Hot Observable 作為 WorkerFragment 的資料來源

推薦大家先看一下這篇文章 RxJava 教程第三部分:馴服資料流之 Hot & Cold Observable,這裡面對於Cold & Hot Observable進行了解釋,它們之間關鍵的區別就是:

  • 只有當訂閱者訂閱時,Cold Observale才開始傳送資料,並且每個訂閱者都獨立執行一遍資料流程式碼。
  • Hot Observable不管有沒有訂閱者,它都會傳送資料流。

而在我們的應用場景中,由於WorkerFragment是在後臺執行任務:

  • Activity的角度來看:每次Activity重建時,在Activity中都需要用一個新的Observer例項去訂閱WorkerFragment中的資料來源,因此我們只能選擇通過Hot Observable,而不是Cold Observable來實現WorkerFragment中的資料來源,否則每次都會重新執行一遍資料流的程式碼,而不是繼續接收它傳送的事件。
  • WorkerFragment的角度來看,它只是一個任務的執行者,不管有沒有人在監聽它的進度,它都應該執行任務。

通過Observable.create方法建立的是一個Cold Observable,該Cold Observable每隔1s傳送一個事件。我們呼叫publish方法來將它轉換為Hot Observable,之後再呼叫該Hot Observableconnect方法讓其對源Cold Observable進行訂閱,這樣源Cold Observable就可以開始執行任務了。並且,通過connect方法返回的Disposable物件,我們就可以管理轉換後的Hot Observable和源Cold Observable之間的訂閱關係。

Disposable的用途在於:在某些時候,我們希望能夠停止源Observable任務的執行,例如當該WorkerFragment真正被銷燬時,也就是執行了它的onDestroy方法,那麼我們就可以通過上面的Disposable取消Hot Observable對源Cold Observable的訂閱,而在Cold Observable的迴圈中,我們判斷如果下游(也就是Hot Observable)取消了訂閱,那麼就不再執行任務。

整個的架構圖如下所示:

RxJava2 實戰知識梳理(10)   螢幕旋轉導致 Activity 重建時恢復任務

2.3 在任務執行之後 removeFragment

在了能讓WorkerFragment能正常進行下一次任務的執行,我們需要在發生錯誤或者任務完成的時候,通過remove的方式銷燬WorkerFragment


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章