Android:聊聊 MVP 中 Presenter 的生命周期

问题背景

前几天写了篇 MVP 入门级的文章,文章结尾有个疑问没有解决,如何避免 Presenter 内存泄漏情况的发生?我查看了一些资料后,发现这个问题可以转成另外一个问题,如何保证 PresenterActivity/Fragment 的生命周期一致?

注:为了方便起见,文中 View 层直接用 Activity/Fragment 代替,有时只说 Activity

我们先来看一个小案例,看看具体问题是什么样。

案例本身很简单,就是模拟耗时加载数据,一点技术含量都没有,只不过是标准的 MVP 模式。

[图片上传失败...(image-4c30c2-1530535722013)]

源码暂时就不贴了,现象为主,什么现象呢?首先我在加载数据时,也就是在 Presenter 那里故意延时了3秒钟,在这3秒钟内,我们对 Activity 做一些邪恶的事情。

1、跳转到另一个 Activity,但并不 Finish 自身:

[图片上传失败...(image-16c13c-1530535722013)]

2、跳转到另一个 Activity,同时 Finish 自身:

[图片上传失败...(image-f42772-1530535722013)]

仔细对比可以看出几个问题:

  • Presenter 所在 Activity 已经不可见了(onStop),Presenter 仍然在作用;
  • Presenter 所在 Activity 已经被杀死了(onDestroy),Presenter 仍然在作用;
  • 页面不可见的时候,竟然没有报空指针,且返回时仍然能看见,显示正常。

这些现象都是因为 Activity 的生命周期变化后 Presenter 没有做对应的处理所造成的,那该怎么办呢?

一些思考

上文说了,问题本质就是如何保证 PresenterActivity/Fragment 的生命周期一致?其实方法有很多,比较成熟的有 MVPro 框架、Beam 框架,还有今天要说的比较有趣且巧妙的方法 Loader 方案。

对于 MVPro 框架,它的思路是:

既然要保证 PresenterActivity 生命周期一致,那不如就把 Activity 作为 Presenter 层,而不是作为 View 层。

这就上升到更深层次的问题:Activity/Fragment 到底是 V 层还是 P 层?

网上各种说法都有,我个人觉得 P 层是 M 层和 V 层的沟通桥梁,而按照我们惯性的想法就是 Activity 是直观呈现给用户的,是与 View 有直接关联的,包括一些展示、输入、更新等等操作都是在 Activity 上完成(至少给我们的直观感觉是这样),那么显然 Activity 更合适在 V 层上。

虽然 MVPro 框架将 Activity 上所有的 View 操作都用一个抽象类来实现,但我仍然不觉得这是一个最优的解决方案(又批判了大神,再逃……)。

Android MVP框架MVPro的使用和源码分析 - 亓斌

而对于 Beam 框架,它的思路是这样的:

PresenterActivity 的绑定关系应由静态类管理,而不是由 Activity 管理。当 Activity 意外重启时 Presenter 不应重启,只需在 Activity 重启时,让 PresenterActivity 重新绑定,并根据数据恢复 Activity 状态即可。而当 Activity 真正销毁时,对应 Presenter 才应该跟随销毁。

这跟设置一个单例的 Application 有点类似,不管 Activity 怎么变化,Application 都只有一个,所以可以通过这个 Application 来管理。不得不说,这个思路我还是比较能接受的,也是我能想出来的最简单的方法。

Android应用中MVP最佳实践 - Jude95

但是回头看一看 MVP 架构的核心思想(原文及图来源):

Activity/Fragment 变成一个单纯的 View ,负责展示数据并将各种事件分发给中间人,也就是 PresenterPresenter 会处理每一个事件,从 Model 层获取或上传数据,并将获得的数据进行处理并让 View 层展示。PresenterActivity/Fragment 的通信,是通过 Activity/Fragment 所继承的 View 接口来间接完成的。

MVP

这就很显而易见了,Activity/Fragment 就是 View 层,所以我们仍然需要一个更好的方法来解决这个问题,让 Presenter 不由 Activity/Fragment 的生命周期来管理。

由于 Presenter 是一个中间的主持者,所以生命周期一定长于或者说至少不短于 Activity/Fragment ,所以这就有两点要求:

  • Presenter 生命周期独立;
  • Presenter 生命周期长于 Activity/Fragment

但我们知道,Presenter 只是我们自己定义的一层中间主持者对象,如果要实现需求还是要绑定一个已有的东西,那 Android 里有什么东西的生命周期是独立且长于 Activity/Fragment 呢?

我首先想到的是 Application ,它的生命周期是独立且大于等于 Activity 的,完全满足需求,而且一般我们做项目的时候都会有一个 Application 类,充当一个全局管理者的角色。但这跟上面的 Beam 框架有点类似,所以暂时不说这个。

那有没有更好的呢?我没想到,但是别人想到了,也就是今天说的 Loader

初识 Loader

关于 Loader 类,其实之前我不太了解,只是在《 Andrid 开发艺术探索》里面见过这个类,当时说的是异步。因为异步这东西现在很多框架都能很好的实现,所以并没在意,但通过几天的学习,觉得这个类还是很 NB 的。

因为文章重点不是这个类,我就简单说一下它的作用及特点,不深入讨论具体的使用方式,有兴趣可以点击文末相关链接学习。

Loader 是啥

一句话概括:

Android 提供的一种支持 Activity/Fragment 的、异步的、带有监听功能的、能自动重连接的加载器。

哦哟,真的很 NB 的样子,来一个一个看。

1、支持 Activity/Fragment

这个意思是不管在 Activity 中还是 Fragment 中,它都能通过 getLoaderManager().initLoader(0, null, this) 来创建自身的管理类。而我们的 View 层的实现刚好是 Activity/Fragment

2、异步

这个词在 Android 中不要太熟悉,在 Loader 的实现中还有一个抽象子类 AsyncTaskLoader 。它的内部提供一个 AsyncTask 进行异步的一些耗时操作。这就很厉害了,因为这个问题的源头就是 Presenter 进行了耗时操作。

3、带有监听功能

这个意思其实就是能够及时响应一个数据的变化并实时更新 UI ,有点类似 ContentObserver ,充当一个观察者的角色。

4、能自动重新连接

我觉得这才是重磅功能,它能够在 Activity/Fragment 发生 Configuration Change 的时候,自动重新连接。比如 Activity 突然横屏了,生命周期发生了巨大变化,这个时候它能够自己处理这些变化,并自动重新连接自身。

说到这里,Loader 的强大之处我们已经能够窥见一丢丢了。

Loader 的生命周期

前面就一直强调生命周期的问题,既然 Loader 满足需求,那就来看看它的生命周期。一般来说,一个完成的 Loader 机制需要三个东西,三者关系如下图所示:

[图片上传失败...(image-1c4b5a-1530535722013)]

下面依次来看:

1、LoaderManager

顾名思义,它是 Loader 的管理者:

  • initLoader():第一个参数是 LoaderId,第二个参数可选,第三个参数为回调的实现类,一般都为当前的 Activity/Fragment
  • restartLoader():其实一般通过 initLoader 都会监测是否存在指定 IdLoader ,如果有就重启一下,但是如果你不想要之前的数据了,就彻底重新一个新的 Loader

2、LoaderManagerCallbacks

从名称就可以看出,这是 LoaderManager 的回调类,里面有三个方法:

  • onCreateLoader():实例化和返回新建给定 IdLoader ;
  • onLoadFinished():当一个创建好的 Loader 完成了 Load 过程,调用此函数;
  • onLoaderReset():当一个创建好的 Loader 要被 Reset ,调用此函数,此时数据无效。

3、Loader

a、生命周期

  • active:活动状态:
    • started:启动状态;
    • stopped:停止状态,有可能再次启动。
  • inactive:非活动状态:
    • abandoned:废弃状态,废弃后过段时间也会重置;
    • reseted:重置状态,表示该 Loader 已经完全被销毁重用了。
Loader 生命周期图

b、onStartLoading

如果 Activity 执行 onStart 方法,Loader 会执行此方法,此时 Loader 处于 started 状态下,Loader 应该监听数据源的变化,并将变化的新数据发送给客户端。 这个时候有两种情况:

  • 已经存在 Loader 所要加载对象实例,应该调用 deliverResult() 方法,触发 onLoadFinished() 回调的执行,从而客户端可以从该回调中轻松获取数据。
  • 如果没有的话,则先触发 onCreateLoader() 回调创建 Loader ,再调用 forceLoad() 方法,去促使它去加载,加载后再调用 deliverResult() 方法,回调 onLoadFinished()

c、onStopLoading

Activity/Fragment 执行 onStop() 方法时,Loader 会调用此方法,此时 Loader 处于 stopped 状态下。而且当 Activity/Fragment 处于 stopped 状态时,所有的 Loader 也会被置于 stopped 状态。

此时应该继续监听数据的变化,但是如果数据有变化应该先存起来,等重新 start 的时候再发送给客户端更新 UI 。

d、onReset

abandoned 状态暂时略过,来看 onReset 这个方法。它会在 Activity/Fragment 销毁时调用(主动调用 destroyLoader() 也可)。触发 onLoaderReset() 回调,并重新创建 Loader ,后续步骤类似 onStartLoading()

回头看看上文会发现它的第四个特点跟它的生命周期密切相关。也就是说,它不管 Activity/Fragment 怎么变化,它自己过它自己的。

为什么选 Loader

通过上文对 Loader 的相关了解,现在来总结一下,为什么 Loader 能够满足这样的需求呢?

  • LoaderAndroid 框架中内部提供的;
  • 每一个 Activity/Fragment 都可以持有自己的 Loader 对象的引用。
  • LoaderActivity/Fragment 状态改变时是不会被销毁的,因为它可以自动重建;
  • Loader 的生命周期是是由系统控制的;
  • Loader 会在 Activity/Fragment 不再被使用后由系统自动回收;
  • LoaderActivity/Fragment 的生命周期绑定,事件自身就能分发;

项目改造

先分析

好了,花了很长的篇幅去简单介绍一下 Loader 。现在回到本质问题上,如何利用 Loader 的相关特性去解决 Presenter 的生命周期问题呢?一句话概括:

Loader 的生命周期内,绑定其所在的 Activity/Fragment 所对应的 Presenter

这就能让 Presenter 独立于 Activity/Fragment 的生命周期之外,这样就不用担心它们生命周期变化所带来的一系列问题。

再动手

现在我们就要对本文开头的那个小案例进行修改了,不过有一点说明:

之前的例子不太好,因为现在 Android 中子线程不能手动 stop ,所以没法演示 Activity 销毁,Presenter 就同步销毁的 案例,所以我将耗时部分加了循环,根据标志位,判断是否循环加载。

好,现在一项一项来:

Bean

数据 Bean 类,太简单,不说:

public class PersonBean {
    private String name;
    private String age;
    // ...省略
}

Model Interface

仍然是为了演示架构强行抽取的,没有实质性意义的方法:

public interface IPersonModel {
    //加载Person信息
    ArrayList<PersonBean> loadPersonInfo();
}

Model

实现 Model Interface ,这里是模拟数据:

public class PersonModel implements IPersonModel {

    //存一下Person的信息
    private ArrayList<PersonBean> personList = new ArrayList<>();
    
    /**
     * 加载Person信息
     *
     * @return 返回信息集合
     */
    @Override
    public ArrayList<PersonBean> loadPersonInfo() {
        personList.add(initPerson());
        return personList;
    }

    private PersonBean initPerson() {
        PersonBean personBean = new PersonBean();
        personBean.setName("张三");
        //...省略
        return personBean;
    }
}

View Interface

View 层必须的 Interface ,这里也就一个方法:

public interface IPersonView {
    //更新UI
    void updateUI(ArrayList<PersonBean> personList);
}

Base Presenter

这里因为我们需要跟 Activity 生命周期挂钩,所以抽取一个 Presenter 的基类:

public interface BasePresenter<V> {
    void onViewAttached(V view);
    void onViewDetached();
    void onDestroyed();
}

Presenter

这里就是重要的 Presenter 的实现了,有几点要注意的:

  • 仍然需要 ModelView 层的接口;
  • 线程是循环的,是否循环通过标记判断;
  • onViewAttached、onViewDetached、onDestroyed 中改变循环标记。
    public class PersonPresenter implements BasePresenter {
    
        private IPersonModel mPersonModel;  //Model接口
        private IPersonView mPersonView;    //View接口
    
        private Handler mHandler = new Handler();   //模拟耗时用的 没实质性作用
        private boolean isLoad = true;              //循环加载标志
    
        public PersonPresenter(IPersonView mPersonView) {
            mPersonModel = new PersonModel();
            this.mPersonView = mPersonView;
        }
    
        public void updateUIByLocal() {
            //Model层处理
            final ArrayList<PersonBean> personList = mPersonModel.loadPersonInfo();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (isLoad) {
                        //模拟1s耗时
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //运行在 Main 线程
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                //View层更新
                                mPersonView.updateUI(personList);
                            }
                        });
                    }
                }
            }).start();
        }
    
        public void onViewAttached(Object view) {
            this.isLoad = true;
            updateUIByLocal();
        }
    
        public void onViewDetached() {
            this.isLoad = false;
        }
    
        public void onDestroyed() {
            this.isLoad = false;
        }
    }

Loader

现在来看 Loader 怎么写,先看源码:

public class PresenterLoader<T extends BasePresenter> extends Loader<T> {

    private PersonPresenter presenter;
    private PresenterFactory factory;

    public PresenterLoader(Context context,PresenterFactory factory) {
        super(context);
        this.factory = factory;
    }

    @Override
    protected void onStartLoading() {

        // 如果已经有Presenter实例那就直接返回
        if (presenter != null) {
            deliverResult((T) presenter);
            return;
        }

        // 如果没有 就促使加载
        forceLoad();
    }

    @Override
    protected void onForceLoad() {
        // 实例化 Presenter
        presenter = factory.create();
        // 返回 Presenter
        deliverResult((T) presenter);
    }

    @Override
    protected void onReset() {
        presenter.onDestroyed();
        presenter = null;
    }
}
  • 这里只继承了最简单的 Loader 类,T 就是各种继承 BasePresenterPresenter
  • 记着 Loader 的生命周期与 Presenter 生命周期一致,所以 Loader 开启,那就要加载 Presenter。如果 Loader 重启,那么 Presenter 就要销毁。
  • 这里有个 PresenterFactory 类,就是为了创建各种 Presenter,具体看下面。

PresenterFactory

这是 Presenter 的工厂类,为了创建各种 Presenter,先看代码怎么写的:

public class PresenterFactory{

    private IPersonView mPersonView;

    public PresenterFactory(IPersonView mPersonView) {
        this.mPersonView = mPersonView;
    }

    public PersonPresenter create() {
        return new PersonPresenter(mPersonView);
    }
}

可以看到很简单,就是 new 一个对应的 Presenter 出来,这里我偷懒了,最好抽取一个接口出来:

public interface PresenterFactory<T extends Presenter> {
    T create();
}   

View

好了,那么最后就是 View 层的实现类 Activity 了,仍然先看代码:

public class MainActivity extends AppCompatActivity implements IPersonView, LoaderManager.LoaderCallbacks<PersonPresenter>, View.OnClickListener {

    /*===== 控制相关 =====*/
    private int i = 0;
    private Toast mToast;
    private PersonPresenter mPersonPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initListener();
        initData();
    }

    //...省略

    private void initData() {
        //得到一个Loader管理者,并创建一个Loader
        getLoaderManager().initLoader(0, null, this);
    }

    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.bt_main_load:
                mPersonPresenter.updateUIByLocal();
                break;
            case R.id.bt_main_goto:
                gotoOther(fsvMainFinish.ismIsOpen());
                break;
        }
    }

    private void gotoOther(boolean isFinish) {
        startActivity(new Intent(this, OtherActivity.class));
        if (isFinish) {
            finish();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        mPersonPresenter.onViewAttached(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        mPersonPresenter.onViewDetached();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPersonPresenter.onDestroyed();
    }

    /**
     * View 接口方法 更新UI
     *
     * @param personList 用户集合
     */
    @Override
    public void updateUI(ArrayList<PersonBean> personList) {
        PersonBean personBean = personList.get(0);
        tvMainName.setText("姓名:" + personBean.getName());
        //...省略
        showToast("第 " + i + " 次加载");
        i++;
    }

    /*========== Loader 的回调方法 ==========*/
    @Override
    public Loader<PersonPresenter> onCreateLoader(int id, Bundle args) {
        //创建
        return new PresenterLoader<>(this, new PresenterFactory(this));
    }

    @Override
    public void onLoadFinished(Loader<PersonPresenter> loader, PersonPresenter presenter) {
        //完成加载
        this.mPersonPresenter = presenter;
    }

    @Override
    public void onLoaderReset(Loader<PersonPresenter> loader) {
        //销毁
        this.mPersonPresenter = null;
    }
}

代码中的核心有这几个部分:

  • 分别实现了 View 层接口和 LoaderManager.LoaderCallbacks 接口,因为前文已经说过 Loader 需要回调;
  • 通过 getLoaderManager().initLoader(0, null, this) 得到一个管理者并新建一个 Id0Loader ,这里我没有用 V4 包,所以没有 support ;
  • Activity 的每个生命周期方法中,调用 Presenter 的相关方法;
  • 实现 LoaderCallbacks 接口的相关回调方法。

看到这里,大家可能有疑问,从代码中看 Presenter 的相关方法确实已经跟 Activity 的生命周期连在一起了,但为什么说是绑定了 Loader 呢?原因如下:

  • Presenter 确实和 Loader 绑定了,因为 Presenter 随着 Loader 的创建/销毁而创建/销毁,并非是 Activity
  • Activity 生命周期中 Presenter 对应的方法并不意味着它和 Presenter 绑定,只是生命周期发生改变需要 Presenter 做出一些变化而已;
  • 这种做法其实也可以说是:利用 Loader 延长了 Presenter 的生命周期。

看结果

现在我们看一下结果:

1、跳转到另一个 Activity,但并不 Finish 自身:

[图片上传失败...(image-9274bc-1530535722013)]

2、跳转到另一个 Activity,同时 finish() 自身:

[图片上传失败...(image-92f4bf-1530535722013)]

从图中我们可以看到两个现象:

  • Presenter 所在 Activity 生命周期发生变化时,Presenter 也会发生对应的变化,图中的变化就是停止循环加载;
  • 当不 finishActivity 并再次返回时,Presenter 仍然可以继续之前停止的位置开始加载,这就是 Loader 的作用。

上面两点说明的问题一句话概括就是:

Presenter 的生命周期已经和 Activity 保持一致了,而且比 Activity 的生命周期还要长,因为它被 Loader 绑定了。

到此,文章开头的疑问也就基本解决了。

总结

回头看了一下,实在没想到写了这么多,有的地方还是啰嗦了,而且案例选择的不太好,不过在原理上已经没啥大问题了。

不过值得注意的是,用 Loader 方案去解决这种问题,并不是完美的,真正实践起来还是有坑的地方,比如 Fragment 里面比较难控制 Loader ,只是思路比较有趣,解决起来也相对比较容易,至少不影响 MVP 的架构,个人来说比较喜欢。

之前也跟 GithubApp 开发者 交流过一次,他的思路是使用一个管理者去管理 Rx 中所有的 Subscription 。要是生命周期变化,对应的 Presenter 就会变化,根据这些变化一些 Subscription 就会被退订,也就防止了内存泄漏的情况。

不管怎么说,还是实践出真知。

参考资料

通过Loader延长Presenter生命周期

深入源码解析Android中Loader、AsyncTaskLoader、CursorLoader、LoaderManager

Android应用Loaders全面详解及源码浅析 - 工匠若水

项目源码

LoaderMVPDemo - IamXiaRui


个人博客:www.iamxiarui.com
原文链接:http://www.iamxiarui.com/?p=903

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

推荐阅读更多精彩内容