[android进阶篇]MVP模式优化,防止内存泄漏和空指针问题
来源:互联网 发布:二叉树层次遍历算法 编辑:程序博客网 时间:2024/06/07 08:11
MVPOptimize MVP模式优化
主要优化P层V层互相持有对象,不能及时回收/销毁问题
如果你看过我的MVP整合教程【android进阶篇】MVP+Retrofit+RxJava框架结合 你可能就会发现,如果页面在请求的时候,网络不好,这时用户跳转到其他页面,就可能会抛出空指针异常/空对象/内存泄露等问题(网上大部分mvp入门教程也存在相同的问题);
尊重原创,转载请注明出处,原文地址: http://blog.csdn.net/qq137722697
内存泄露追踪
手动调用finish();方法销毁当前页面
如果你的业务中在进入下一个页面时,会把当前页面销毁,此时你会调用finish();方法,但是其实是有问题,来看看这张MVP结构图:
M 层与 P 层是互相持有对象的关系;通过代码也可以看出来:
View层持有Presenter层:
public class LoginActivity extends BaseMvpActivity implements LoginContact.ILoginView { ... private LoginContact.ILoginPresenter presenter; ... }
Presenter层持有View层对象:
public class LoginPresenterIml implements LoginContact.ILoginPresenter { ... private LoginContact.ILoginView loginView; ...}
为什么要表明他们互相持有呢?
因为即使页面finish之后,如果Presenter层还持有View层的引用,jvm不会马上回收,也就是说finish之后页面没有真正意义上的销毁;
那么问题就来了,既然没有销毁,如果这样没有销毁的页面太多,就会造成内存泄露;
解决建议
既然finish();之后不会真正销毁是因为它们互相持有对象,那就把这种关系打破即可,怎么做呢?
1、在页面(activity/fragment)回调onDestroy()方法的时候,通知presenter断开持有View层的引用;并将presenter对象赋值为null,调用System.gc();通知jvm;
2、presenter在收到通知销毁页面的时候,将view赋值为null,并且调用System.gc();通知jvm
- [推荐] 在presenter收到通知销毁页面的时候,有条件的话可以增加一个取消正在执行任务的方法(此方法非必须,继续往下看);
特别说明:
将对象赋值为null,会断开对象的引用;
将presenter赋值为null,会断开View持有Presenter对象的引用
同样的将view赋值为null,会断开presenter持有view对象的引用
调用System.gc()通知jvm可以回收垃圾了,jvm并不会马上回收,待jvm”心情好”的时候会自动回收;
来看看代码实现:(后面有完整代码)
1、view的onDestroy方法通知销毁页面,并设置presenter为null
@Override public void onDestroy() { super.onDestroy(); if (presenter != null) { presenter.onDestroy(); presenter = null; System.gc(); } }
2、presenter解除持有view对象
public class LoginPresenterIml implements LoginContact.ILoginPresenter { ... /** * 当页面销毁的时候,需要把View=null, * 然后调用 System.gc();//尽管不会马上回收,只是通知jvm可以回收了,等jvm高兴就会回收 */ @Override public void onDestroy() { loginView = null; System.gc(); }}
以上操作是重复代码,考虑抽取,继续往下看
空指针/空对象问题
场景一
通过以上两个操作,jvm会适时回收view层,但是如果presenter层还在继续做耗时操作的话不会马上被回收,此时如果view已经被回收,耗时操作刚好完成要通知view层做更新ui的操作,那么就会出现空指针/空对象的异常;
比如:获取网络数据(耗时操作)成功后,需要展示到ui上,此时会调用view.showData(result)方法,view对象为空,就抛出异常了;
场景二
还有一种情况也会出现空指针/空对象的问题,就是jvm在内存吃紧的时候会回收不可见的view;你可能会说上面不是在页面(activity/fragment)的onDestory()方法调用的时候通知销毁对象嘛!
其实,jvm在回收页面的时候不会保证回调onDestroy方法的,所以就不能及时通知presenter销毁了。
解决建议
这个问题很好解决:presenter中,在所有需要使用view对象之前加一个非空判断,如果为空直接return;不在做任何操作
例子:
public class LoginPresenterIml implements LoginContact.ILoginPresenter { ... /** * 开始登录 * * @param username 用户名 * @param pwd 密码 */ @Override public void startLogin(String username, String pwd) { loginModel.login(username, pwd, new BaseCallbackListener<LoginResult>() { @Override public void onStart() { //拿到结果之后判断v层是否已经销毁,防止空对象 if (loginView == null) { Log.e("hdltag", "onStart(LoginPresenterIml.java:37):页面已经销毁,不在进行任何操作"); return; } loginView.showLoading(); } @Override public void onNext(LoginResult result) { //拿到结果之后判断v层是否已经销毁,防止空对象 if (loginView == null) { Log.e("hdltag", "onStart(LoginPresenterIml.java:47):页面已经销毁,不在进行任何操作"); return; } loginView.closeLoading(); switch (result.getCode()) { case "0": loginView.showMsg(result.getMsg()); loginView.toMainPage(); break; case "10002001": loginView.showMsg(result.getMsg()); break; default: } } @Override public void onError(Throwable errorMsg) { //拿到结果之后判断v层是否已经销毁,防止空对象 if (loginView == null) { Log.e("hdltag", "onStart(LoginPresenterIml.java:67):页面已经销毁,不在进行任何操作"); return; } loginView.closeLoading(); loginView.showMsg(errorMsg.getMessage()); } }); }}
总结
通过以上两个方法基本上可以避免大部分mvp中的内存泄露和空指针异常问题,对于一些重复的操作我们可以这样抽取一下:
既然每个页面都需要在onDestroy方法中通知presenter销毁对象,那就抽取一个公共的父类;
BasePresenter
每个presenter对象都有销毁对象的方法,所以抽取一个公共的接口类BasePresenter并定义个onDestroy方法,以后所有的presenter都需要实现这个接口
/** * 公共Presenter类 * Created by HDL on 2017/10/17. * * @author HDL */public interface BasePresenter { /** * 当页面销毁的时候,需要把View=null, * 然后调用 System.gc();//尽管不会马上回收,只是通知jvm可以回收了,等jvm高兴就会回收 */ void onDestroy();}
BaseMvpActivity
继续来抽取一下每个页面都需要调用的onDestroy()方法,所以抽取一个公共的BaseMvpActivity类,这个类重写onDestroy方法,在这个方法中实现调用presenter.onDestory()和断开presenter引用的操作;
实际开发中如果你的项目中已经有BaseActivity了,那只需要将BaseMvpActivity继承的AppCompatActivity改成你自己的BaseActivity即可
/** * 实现mvp模式的公共activity类,所有使用mvp的activity页面都需要继承此类 * <p> * 温馨提示: * 1、实际开发中需要将BaseMvpActivity继承的AppCompatActivity改成你自己的BaseActivity(如果有) * 2、如果页面再进去其他页面之后不需要了,一定要及时finish * <p> * Created by HDL on 2017/10/17. * * @author HDL */public abstract class BaseMvpActivity extends AppCompatActivity implements View.OnClickListener { private BasePresenter presenter = null; public Context mContext; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(getLayoutResId()); mContext = this; presenter = bindPresenter(); initView(); initData(); } /** * 返回资源的布局 * * @return */ public abstract int getLayoutResId(); /** * 组件初始化操作 */ public abstract void initView(); /** * 页面初始化页面数据,在initView之后调用 */ public abstract void initData(); /** * 绑定presenter,主要用于销毁工作 * * @return */ protected abstract BasePresenter bindPresenter(); /** * 如果重写了此方法,一定要调用super.onDestroy(); */ @Override public void onDestroy() { super.onDestroy(); if (presenter != null) { presenter.onDestroy(); presenter = null; System.gc(); } }}
里面的getLayoutResId()、initView()、initData()方法都是为了规范页面的写法而抽取的(非必须抽取),如果觉得麻烦可以不抽取;
- BaseMvpFragment
同理Fragment也可以抽取一下
/** * 实现mvp模式的公共Fragment类,所有使用mvp的fragment都需要继承至这个类 * <p> * 温馨提示: * 1、实际开发中需要将BaseMvpFragment继承的Fragment改成你自己的BaseFragment(如果有) * 2、如果页面再进去其他页面之后不需要了,一定要及时finish * <p> * Created by HDL on 2017/10/17. * * @author HDL */public abstract class BaseMvpFragment extends Fragment { private BasePresenter presenter = null; public Context mContext; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mContext = getActivity(); View view = View.inflate(mContext, getLayoutResId(), null); presenter = bindPresenter(); initView(view); initData(); return view; } /** * 返回资源的布局 * * @return */ public abstract int getLayoutResId(); /** * 组件初始化操作 * * @param view 父view */ public abstract void initView(View view); /** * 页面初始化页面数据,在initView之后调用 */ public abstract void initData(); /** * 绑定presenter,主要用于销毁工作 * * @return */ protected abstract BasePresenter bindPresenter(); /** * 如果重写了此方法,一定要调用super.onDestroy(); */ @Override public void onDestroy() { super.onDestroy(); if (presenter != null) { presenter.onDestroy(); presenter = null; System.gc(); } }}
公共的方法抽取好之后来看一个登录的例子:
目录结构
登录界面
接口管理类LoginContact:
/** * MVP接口管理类 * Created by HDL on 2017/10/18. * * @author HDL */public class LoginContact { /** * M层 */ public interface ILoginModel { /** * 实际登录的地方,调用服务器登录接口 * * @param username 用户名 * @param pwd 密码 * @param callbackListener 登录结果回调 */ void login(String username, String pwd, BaseCallbackListener<LoginResult> callbackListener); } /** * V层 */ public interface ILoginView { /** * 显示提示消息 * * @param msg */ void showMsg(CharSequence msg); /** * 显示加载中 */ void showLoading(); /** * 管理加载状态 */ void closeLoading(); /** * 跳转到主页面(登录成功之后) */ void toMainPage(); } /** * P层 */ public interface ILoginPresenter extends BasePresenter{ /** * 开始登录 * * @param username 用户名 * @param pwd 密码 */ void startLogin(String username, String pwd); }}
实际业务处理类(Model层)LoginModelIml:
/** * 实际业务处理类 * Created by HDL on 2017/10/18. * * @author HDL */public class LoginModelIml implements LoginContact.ILoginModel { /** * 实际登录的地方,调用服务器登录接口 * * @param username 用户名 * @param pwd 密码 * @param callbackListener 登录结果回调 */ @Override public void login(final String username, final String pwd, final BaseCallbackListener<LoginResult> callbackListener) { //登录请求 HttpSend.getInstance().login(username,pwd,callbackListener); }}
由于没有测试环境,所以来模拟登录的过程,改造一下
/** * 实际业务处理类 * Created by HDL on 2017/10/18. * * @author HDL */public class LoginModelIml implements LoginContact.ILoginModel { //测试代码,正式环境中不使用这个 private BaseCallbackListener<LoginResult> callbackListener; //测试代码,正式环境中不使用这个 private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: callbackListener.onError(new Throwable("登录失败,请检查网络连接")); break; case 1: callbackListener.onNext((LoginResult) msg.obj); break; case 2: callbackListener.onStart(); break; default: } } }; /** * 实际登录的地方,调用服务器登录接口 * * @param username 用户名 * @param pwd 密码 * @param callbackListener 登录结果回调 */ @Override public void login(final String username, final String pwd, final BaseCallbackListener<LoginResult> callbackListener) { /* * 1、实际开发中需要调用登录只要调用登录接口即可,如以下的登录代码: * HttpSend.getInstance().login(username,pwd,callbackListener); * 2、下面是模拟操作,实际开发中使用类似上面的代码 */ //测试代码,正式环境中不使用这个 this.callbackListener = callbackListener; new Thread() { @Override public void run() { mHandler.sendEmptyMessage(2); //模拟任务执行失败 if (username.contains("e")) { mHandler.sendEmptyMessage(0); return; } try { //模拟网速比较慢,要10s才能请求到结果 Thread.sleep(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } LoginResult result = new LoginResult(); if ("mvp".equals(username) && "132".equals(pwd)) { //0表示成功 result.setCode("0"); result.setMsg("登录成功"); } else { //10002001表示用户或密码错误 result.setCode("10002001"); result.setMsg("用户名或密码错误"); } Message msg = mHandler.obtainMessage(); msg.what = 1; msg.obj = result; mHandler.sendMessage(msg); } }.start(); }}
实际逻辑处理类(Presenter层)LoginPresenterIml:
public class LoginPresenterIml implements LoginContact.ILoginPresenter { private LoginContact.ILoginView loginView; private LoginContact.ILoginModel loginModel; public LoginPresenterIml(LoginContact.ILoginView loginView) { this.loginView = loginView; loginModel = new LoginModelIml(); } /** * 开始登录 * * @param username 用户名 * @param pwd 密码 */ @Override public void startLogin(String username, String pwd) { loginModel.login(username, pwd, new BaseCallbackListener<LoginResult>() { @Override public void onStart() { //拿到结果之后判断v层是否已经销毁,防止空对象 if (loginView == null) { Log.e("hdltag", "onStart(LoginPresenterIml.java:37):页面已经销毁,不在进行任何操作"); return; } loginView.showLoading(); } @Override public void onNext(LoginResult result) { //拿到结果之后判断v层是否已经销毁,防止空对象 if (loginView == null) { Log.e("hdltag", "onStart(LoginPresenterIml.java:47):页面已经销毁,不在进行任何操作"); return; } loginView.closeLoading(); switch (result.getCode()) { case "0": loginView.showMsg(result.getMsg()); loginView.toMainPage(); break; case "10002001": loginView.showMsg(result.getMsg()); break; default: } } @Override public void onError(Throwable errorMsg) { //拿到结果之后判断v层是否已经销毁,防止空对象 if (loginView == null) { Log.e("hdltag", "onStart(LoginPresenterIml.java:67):页面已经销毁,不在进行任何操作"); return; } loginView.closeLoading(); loginView.showMsg(errorMsg.getMessage()); } }); } /** * 当页面销毁的时候,需要把View=null, * 然后调用 System.gc();//尽管不会马上回收,只是通知jvm可以回收了,等jvm高兴就会回收 */ @Override public void onDestroy() { Log.e("hdltag", "onStart(LoginPresenterIml.java:82):View已经被销毁了"); loginView = null; System.gc(); }}
View层LoginActivity:
/** * 登录页面 * * @author HDL */public class LoginActivity extends BaseMvpActivity implements LoginContact.ILoginView { /** * 用户名 */ private EditText etUsername; /** * 密码 */ private EditText etPwd; /** * 登录按钮 */ private Button btnLogin; private ProgressDialog mProgressDialog; private TextView tvToRegistePage; private LoginContact.ILoginPresenter presenter = new LoginPresenterIml(this); /** * 返回资源的布局 * * @return */ @Override public int getLayoutResId() { return R.layout.activity_login; } /** * 组件初始化操作 */ @Override public void initView() { etUsername = (EditText) findViewById(R.id.et_login_username); etPwd = (EditText) findViewById(R.id.et_login_pwd); tvToRegistePage = (TextView) findViewById(R.id.tv_login_to_regist); btnLogin = (Button) findViewById(R.id.btn_login_start); btnLogin.setOnClickListener(this); tvToRegistePage.setOnClickListener(this); mProgressDialog = new ProgressDialog(mContext); mProgressDialog.setMessage("登录中....."); } /** * 页面初始化页面数据,在initView之后调用 */ @Override public void initData() { } /** * 绑定presenter,主要用于销毁工作 * * @return */ @Override protected BasePresenter bindPresenter() { return presenter; } /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_login_start: String username = getUsername(); if (TextUtils.isEmpty(username)) { etUsername.requestFocus(); showMsg("用户名不能为空"); return; } String pwd = getPwd(); if (TextUtils.isEmpty(pwd)) { etPwd.requestFocus(); showMsg("密码不能为空"); return; } presenter.startLogin(username, pwd); break; case R.id.tv_login_to_regist: toRegistePage(); default: } } /** * 获取用户输入的用户名 * * @return */ public String getUsername() { return etUsername.getText().toString().trim(); } /** * 获取用户输入并自动加密后的密码 * * @return */ public String getPwd() { String pwd = etPwd.getText().toString().trim(); try { //模拟加密 pwd = URLEncoder.encode(pwd, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return pwd; } /** * 显示提示消息 * * @param msg */ @Override public void showMsg(CharSequence msg) { Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); } /** * 显示加载中 */ @Override public void showLoading() { mProgressDialog.show(); } /** * 管理加载状态 */ @Override public void closeLoading() { mProgressDialog.dismiss(); } /** * 跳转到主页面(登录成功之后) */ @Override public void toMainPage() { startActivity(new Intent(this, MainActivity.class)); } /** * 去注册页面 */ public void toRegistePage() { startActivity(new Intent(this, RegisteActivity.class)); }}
输出结果:
Demo地址:https://github.com/huangdali/MVPOptimize/
尊重原创,转载请注明出处,原文地址: http://blog.csdn.net/qq137722697
- [android进阶篇]MVP模式优化,防止内存泄漏和空指针问题
- MVP模式的ListView展示数据,防止内存泄漏
- Android 内存泄漏及优化防止OOM——进阶必看篇
- Android性能优化--防止内存泄漏
- BaseAdapter优化防止内存泄漏
- Android开发:浅谈MVP模式应用与内存泄漏
- Android开发:浅谈MVP模式应用与内存泄漏
- Android开发:浅谈MVP模式应用与内存泄漏
- MVP模式优化与进阶
- 防止handler内存泄漏问题
- Android 内存泄漏和优化(上)
- 内存泄漏、资源泄漏、空指针等问题的分析与总结
- 浅谈MVP模式应用与内存泄漏
- 拦截器模版附带MVP防止内存泄漏
- Java 性能优化(防止内存泄漏)
- MVP中存在的内存泄漏问题
- 如何解决MVP内存泄漏的问题
- 空悬指针、野指针、内存泄漏、内存溢出
- CSDN-markdown编辑器使用方法
- C/C++操作数重载函数标准库实现
- 详述「设计模式」及其 Java 实现
- pod 清除缓存
- Android 屏幕常亮
- [android进阶篇]MVP模式优化,防止内存泄漏和空指针问题
- LaTex公式符号
- “我们为什么要录用你?”应聘者如何回答?
- Python3pandas库DataFrame用法(基础整理)
- 如何开始CTF
- Bootstrap学习记录,快速手册
- 统计学 数据的概括性度量
- A Prototype Discovery Environment for Analyzing and Visualizing Terascale Turbulent Fluid Flow Simul
- 编译Java文件并生成jar包