问题背景
前几天写了篇 MVP 入门级的文章,文章结尾有个疑问没有解决,如何避免 Presenter 内存泄漏情况的发生?我查看了一些资料后,发现这个问题可以转成另外一个问题,如何保证 Presenter 与Activity/Fragment 的生命周期一致?
注:为了方便起见,文中 View 层直接用 Activity/Fragment 代替,有时只说 Activity 。
我们先来看一个小案例,看看具体问题是什么样。
案例本身很简单,就是模拟耗时加载数据,一点技术含量都没有,只不过是标准的 MVP 模式。
源码暂时就不贴了,现象为主,什么现象呢?首先我在加载数据时,也就是在 Presenter 那里故意延时了3秒钟,在这3秒钟内,我们对 Activity 做一些邪恶的事情。
1、跳转到另一个 Activity,但并不 Finish 自身:
2、跳转到另一个 Activity,同时 Finish 自身:
仔细对比可以看出几个问题:
- Presenter 所在 Activity 已经不可见了(onStop),Presenter 仍然在作用;
- Presenter 所在 Activity 已经被杀死了(onDestroy),Presenter 仍然在作用;
- 页面不可见的时候,竟然没有报空指针,且返回时仍然能看见,显示正常。
这些现象都是因为 Activity 的生命周期变化后 Presenter 没有做对应的处理所造成的,那该怎么办呢?
一些思考
上文说了,问题本质就是如何保证 Presenter 与 Activity/Fragment 的生命周期一致?其实方法有很多,比较成熟的有 MVPro 框架、Beam 框架,还有今天要说的比较有趣且巧妙的方法 Loader 方案。
对于 MVPro 框架,它的思路是:
既然要保证 Presenter 与 Activity 生命周期一致,那不如就把 Activity 作为 Presenter 层,而不是作为 View 层。
这就上升到更深层次的问题:Activity/Fragment 到底是 V 层还是 P 层?
网上各种说法都有,我个人觉得 P 层是 M 层和 V 层的沟通桥梁,而按照我们惯性的想法就是Activity 是直观呈现给用户的,是与 View 有直接关联的,包括一些展示、输入、更新等等操作都是在 Activity 上完成(至少给我们的直观感觉是这样),那么显然 Activity 更合适在 V 层上。
虽然 MVPro 框架将 Activity 上所有的 View 操作都用一个抽象类来实现,但我仍然不觉得这是一个最优的解决方案(又批判了大神,再逃……)。
Android MVP框架MVPro的使用和源码分析 - 亓斌
而对于 Beam 框架,它的思路是这样的:
Presenter 与 Activity 的绑定关系应由静态类管理,而不是由 Activity 管理。当 Activity 意外重启时 Presenter 不应重启,只需在 Activity 重启时,让 Presenter 与 Activity 重新绑定,并根据数据恢复 Activity 状态即可。而当 Activity 真正销毁时,对应 Presenter 才应该跟随销毁。
这跟设置一个单例的 Application 有点类似,不管 Activity 怎么变化,Application 都只有一个,所以可以通过这个 Application 来管理。不得不说,这个思路我还是比较能接受的,也是我能想出来的最简单的方法。
但是回头看一看 MVP 架构的核心思想(原文及图来源):
将 Activity/Fragment 变成一个单纯的 View ,负责展示数据并将各种事件分发给中间人,也就是 Presenter 。Presenter 会处理每一个事件,从 Model 层获取或上传数据,并将获得的数据进行处理并让 View 层展示。Presenter 与 Activity/Fragment 的通信,是通过Activity/Fragment 所继承的 View 接口来间接完成的。
这就很显而易见了,Activity/Fragment 就是 View 层,所以我们仍然需要一个更好的方法来解决这个问题,那换个思路,能不能让 **Presenter** 不由 **Activity/Fragment** 的生命周期来管理呢?
由于 Presenter 是一个中间的主持者,所以生命周期一定长于或者说至少不短于Activity/Fragment ,所以这就有两点要求:
- Presenter 生命周期独立;
- Presenter 生命周期不短于 Activity/Fragment 。
但我们知道,Presenter 只是我们自己定义的一层中间主持者对象,如果要实现需求还是要绑定一个已有的东西,那 Android 里有什么东西的生命周期是独立且长于 Activity/Fragment 呢?
我首先想到的是 Application ,它的生命周期是独立且大于等于 Activity 的,完全满足需求,而且一般我们做项目的时候都会有一个 Application 类,充当一个全局管理者的角色。但这跟上面的Beam 框架有点类似,所以暂时不说这个。
那有没有更好的呢?我没想到,但是别人想到了,也就是今天说的 Loader 。
初识 Loader
关于 Loader 类,其实之前我不太了解,只是在《 Android 开发艺术探索》里面见过这个类,当时说的是异步。因为异步这东西现在很多框架都能很好的实现,所以并没在意,但通过几天的学习,觉得这个类还是很 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 机制需要三个东西,三者关系如下图所示:
下面依次来看:
1、LoaderManager
顾名思义,它是 Loader 的管理者:
- initLoader():第一个参数是 Loader 的 Id,第二个参数可选,第三个参数为回调的实现类,一般都为当前的 Activity/Fragment。
- restartLoader():其实一般通过 initLoader 都会监测是否存在指定 Id 的 Loader ,如果有就重启一下,但如果你不想要之前的数据了,就彻底重建一个新的 Loader 。
2、LoaderManagerCallbacks
从名称就可以看出,这是 LoaderManager 的回调类,里面有三个方法:
- onCreateLoader():实例化和返回新建给定 Id 的 Loader ;
- onLoadFinished():当一个创建好的 Loader 完成了 Load 过程,调用此函数;
- onLoaderReset():当一个创建好的 Loader 要被 Reset ,调用此函数,此时数据无效。
3、Loader
a、生命周期
- active:活动状态:
- started:启动状态;
- stopped:停止状态,有可能再次启动。
- inactive:非活动状态:
- abandoned:废弃状态,废弃后过段时间也会重置;
- reseted:重置状态,表示该 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 能够满足这样的需求呢?
- Loader 是 Android 框架中内部提供的;
- 每一个 Activity/Fragment 都可以持有自己的 Loader 对象的引用;
- Loader 在 Activity/Fragment 状态改变时是不会被销毁的,因为它可以自动重建;
- Loader 的生命周期是是由系统控制的;
- Loader 会在 Activity/Fragment 不再被使用后由系统自动回收;
- Loader 与 Activity/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 的实现了,有几点要注意的:
- 仍然需要 Model 和 View 层的接口;
- 线程是循环的,是否循环通过标记判断;
- 在 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 就是各种继承 BasePresenter 的 Presenter。
- 记着 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) 得到一个管理者并新建一个 Id 为 0 的Loader ,这里我没有用 V4 包,所以没有 support ;
- 在 Activity 的每个生命周期方法中,调用 Presenter 的相关方法;
- 实现 LoaderCallbacks 接口的相关回调方法。
看到这里,大家可能有疑问,从代码中看 Presenter 的相关方法确实已经跟 Activity 的生命周期连在一起了,但为什么说是绑定了 Loader 呢?原因如下:
- Presenter 确实和 Loader 绑定了,因为 Presenter 随着 Loader 的创建/销毁而创建/销毁,并非是 Activity;
- Activity 生命周期中 Presenter 对应的方法并不意味着它和 Presenter 绑定,只是生命周期发生改变需要 Presenter 做出一些变化而已;
- 这种做法其实也可以说是:利用 Loader 延长了 Presenter 的生命周期。
看结果
现在我们看一下结果:
1、跳转到另一个 Activity,但并不 Finish 自身:
2、跳转到另一个 Activity,同时 finish() 自身:
从图中我们可以看到两个现象:
- 当 Presenter 所在 Activity 生命周期发生变化时,Presenter 也会发生对应的变化,图中的变化就是停止循环加载;
- 当不 finish 掉 Activity 并再次返回时,Presenter 仍然可以继续之前停止的位置开始加载,这就是 Loader 的作用。
上面两点说明的问题一句话概括就是:
Presenter 的生命周期已经和 Activity 保持一致了,而且比 Activity 的生命周期还要长,因为它被 Loader 绑定了。
到此,文章开头的疑问也就基本解决了。
总结
回头看了一下,实在没想到写了这么多,有的地方还是啰嗦了,而且案例选择的不太好,不过在原理上已经没啥大问题了。
不过值得注意的是,用 Loader 方案去解决这种问题,并不是完美的,真正实践起来还是有坑的地方,比如 Fragment 里面比较难控制 Loader ,只是思路比较有趣,解决起来也相对比较容易,至少不影响 MVP 的架构,个人来说比较喜欢。
之前也跟 GithubApp 开发者 交流过一次,他的思路是使用一个管理者去管理 Rx 中所有的Subscription 。要是生命周期变化,对应的 Presenter 就会变化,根据这些变化一些Subscription 就会被退订,也就防止了内存泄漏的情况。
不管怎么说,还是实践出真知。
参考资料
深入源码解析Android中Loader、AsyncTaskLoader、CursorLoader、LoaderManager
Android应用Loaders全面详解及源码浅析 - 工匠若水
项目源码
Comments | 7 条评论
hi,首先仔细拜读过文章。文章写得非常好。但这里依然存在一些问题:我根据提供的源码编写工程后,mainActivity在manifest中不做任何的配置,同时启用手机的旋转屏幕。当手机旋转屏幕后,activity重建,loader进如入的生命周期与activity第一次创建时而有所不同。包含如下:1.之前的旧的activity会销毁,但onLoaderReset方法没有被回调2.新的activity创建,但onCreateLoader方法没有被执行 3.present中线程继续执行,也弹出toast提示相应信息,但界面上的textview文本不会更新,经查是由于acticity已经创建了新的,但present中依然持有的是旧的activity(IPersonView)引用,也就是出现了内存泄漏。
解决3的办法是使用 onViewAttached(V view)中传入的view进行替换,使presenter关联最新的view。间接回答了之前留言的同学的疑问。
测试环境:android 6.0 手机:联想k3 note
抽象出的Presenter接口中的void onViewAttached(V view);参数V view 是否有必要啊,好像没有用到,对这点比较疑惑
@墨痕坊 : 你好,这是必要的,因为 onViewAttached(Object view) 方法的用途就是在 View 层(可以理解为 Activity)生命周期变化的时候,Presenter 进行相应的处理。源码中 MainActivity 中的 onResume() 方法中的 mPersonPresenter.onViewAttached(this) 可以看到,传递的是 this ,也就是 MainActivity 本身。如果还是不理解的话,可以看一下 Fragment 的生命周期,也有个 onAttached 方法,异曲同工。
@半夏的大大卷 : 明白了,之前局限于这个demo,没有想更多,谢谢了
@墨痕坊 : 请问,Factory以及Factory中的View没看到有手动置空,Factory由Loader持有,而Loader生命周期比View要长,这个是为何?
presenter最最最不应该就是与application生命周期绑定,因为它不止是为了释放引用,还需要组织当前请求回调对view的操作,因为该view销毁后,再去对view进行操作会出现空指针异常
@细肉云吞 : 额…不知道你是否明白文章的意思,文章的重点是要解决 Presenter 引起的内存泄漏问题,你说的那个空指针异常其实不会出现,因为当 Presenter 对应的 View 销毁后,假如 Presenter 还持有 View 层的引用的话,这个时候出现的是内存泄漏问题,并非是空指针问题。所以解决办法就是在 View 销毁的时候 Presenter 也需要销毁,可是 Presenter 持有 View 层,很难处理好这样的关系,当 View 的具体实现是 Fragment 的话更复杂了,所以网上才有那么多的开源框架来解决这个问题。说到底,根本办法就是彻底不让 View 层参与 Presenter 的生命周期,由另外的管理者来管理 Presenter ,比如文中的 Loader 或者 Application ,至于 Application 是否真的合适解决这样的问题并不是真正的问题所在,重点是解决此类问题的思路。