RxJava2 实战知识梳理(10) - 屏幕旋转导致 Activity 重建时恢复任务

RxJava2 实战系列文章

RxJava2 实战知识梳理(1) - 后台执行耗时操作,实时通知 UI 更新
RxJava2 实战知识梳理(2) - 计算一段时间内数据的平均值
RxJava2 实战知识梳理(3) - 优化搜索联想功能
RxJava2 实战知识梳理(4) - 结合 Retrofit 请求新闻资讯
RxJava2 实战知识梳理(5) - 简单及进阶的轮询操作
RxJava2 实战知识梳理(6) - 基于错误类型的重试请求
RxJava2 实战知识梳理(7) - 基于 combineLatest 实现的输入表单验证
RxJava2 实战知识梳理(8) - 使用 publish + merge 优化先加载缓存,再读取网络数据的请求过程
RxJava2 实战知识梳理(9) - 使用 timer/interval/delay 实现任务调度
RxJava2 实战知识梳理(10) - 屏幕旋转导致 Activity 重建时恢复任务
RxJava2 实战知识梳理(11) - 检测网络状态并自动重试请求
RxJava2 实战知识梳理(12) - 实战讲解 publish & replay & share & refCount & autoConnect
RxJava2 实战知识梳理(13) - 如何使得错误发生时不自动停止订阅关系
RxJava2 实战知识梳理(14) - 在 token 过期时,刷新过期 token 并重新发起请求
RxJava2 实战知识梳理(15) - 实现一个简单的 MVP + RxJava + Retrofit 应用


一、前言

如果我们在AndroidManifest.xml中声明Activity时,没有对android:configChanges进行特殊的声明,那么在屏幕旋转时,会导致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();
    }
}

我们来演示一下屏幕旋转时的效果,在屏幕旋转之后,我们仍然可以继续收到任务进度的更新:


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)取消了订阅,那么就不再执行任务。

整个的架构图如下所示:


2.3 在任务执行之后 removeFragment

在了能让WorkerFragment能正常进行下一次任务的执行,我们需要在发生错误或者任务完成的时候,通过remove的方式销毁WorkerFragment


更多文章,欢迎访问我的 Android 知识梳理系列:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容