写在前面
我在第一次接触架构设计的时候,就被教育架构设计最重要的是思想。思想是指什么,思想就是那张刻在脑海的示意图。而架构设计的最终目的就是为了解耦,提升系统的扩展性。所以,思想特别重要。
MVP示意图如下:
也就是说,只要是满足上面那张图,就是MVP。
MVP的具体解释可参考:
MVP第一篇——MVP、MVVM、MVC的简单介绍和对比 - 简书 https://www.jianshu.com/p/ef34586d0f9d
-
MVP在Android中的简单使用
这是一个简单的例子:
步骤一:
设立项目的结构,如下图:
我是将MVP的三层分成了三个包;这样可以清楚的明确每个包的用途。当然你也可以向官方Dome一样采取按功能模块分层的方式,都可以。
步骤二:
先把View的这部分给写好,首先是xml中我们先写一个ListView:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.activity.MainActivity">
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/main_lv">
</ListView>
</RelativeLayout>
然后我们把ListView绑定到Activity中去;
private ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.main_lv);
}
在MVP中,View的部分是Activity;他最大的责任是展示View。所以我们现在来写一个interface来定义MvpView的基本行为。
MvpView如下:
package com.lay.mvpdemobase.view.iview;
import java.util.List;
/**
* 负责更新展示View
*/
public interface MvpView {
void showView(List<String> list);
}
接着,让MainActivity实现MvpView;
package com.lay.mvpdemobase.view.activity;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.lay.mvpdemobase.R;
import com.lay.mvpdemobase.presenter.MainAcPresenter;
import com.lay.mvpdemobase.view.iview.MvpView;
import java.util.List;
/**
* 在MVP的架构中,Activity是属于View的,所以View负责展示View和更新View
*/
public class MainActivity extends Activity implements MvpView {
private ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.main_lv);
}
/**
* 这个方法是更新展示View
* @param list
*/
@Override
public void showView(List list) {
}
}
我们实现showView方法;
/**
* 这个方法是更新展示View
* @param list
*/
@Override
public void showView(List list) {
//Arrayadapter的几个参数?
ArrayAdapter arrayAdapter=new ArrayAdapter(this,android.R.layout.simple_list_item_1,list);
listView.setAdapter(arrayAdapter);
}
至此,我们完成了MVP中View部分的责任,即为做好了接收数据,更新展示View的准备。
接下来,轮到Model;Model也有固定的行为,即得到数据。所以我们现在来写一个interface来定义MvpModel的行为。
MvpModel如下:
package com.lay.mvpdemobase.model;
import java.util.List;
/**
* 负责获取数据
*/
public interface MvpModel {
void getData(OnLoadCompleteListener onLoadCompleteListener);//得到数据
public interface OnLoadCompleteListener {//建立一个监听的接口对象来判断
void onLoadComplete(List<String> stringList);//拿到数据
}
}
这个地方是两个接口回调。第一个接口可以通过 类implements接口的方式来实现;第二个接口回调可以在具体使用的场景中使用匿名内部类来实现。
第一个接口实现,MvpModel如下:
package com.lay.mvpdemobase.model;
import java.util.ArrayList;
import java.util.List;
public class MainAcModel implements MvpModel {
@Override
public void getData(OnLoadCompleteListener onLoadCompleteListener) {
//1.得到数据
//模拟得到数据
List<String> stringList=new ArrayList<>();
//快键键收藏
for (int i = 0; i < 100; i++) {
stringList.add("这个是第"+i+"个数据");
}
if(onLoadCompleteListener!=null){
onLoadCompleteListener.onLoadComplete(stringList);
}
}
}
此时,Model的基本行为,即得到数据的功能已经完成。
而第二个接口的实现部分在Presenter部分,所以我们放到后面来说。
现在只剩下Presenter的部分,即连接上述两者的部分还没有完成,下面是Present部分:
我们直接写一个专门针对MainActivity的Presenter:
package com.lay.mvpdemobase.presenter;
import com.lay.mvpdemobase.model.MainAcModel;
import com.lay.mvpdemobase.model.MainAcModel2;
import com.lay.mvpdemobase.model.MvpModel;
import com.lay.mvpdemobase.view.iview.MvpView;
import java.util.List;
public class MainAcPresenter {
private MvpModel mvpModel;
private MvpView mvpView;
public MainAcPresenter(MvpView mvpView) {
this.mvpView = mvpView;
}
//直接写一个setModel方法就可以了
public MainAcPresenter setMvpModel(int model){
switch (model){
case 0:
mvpModel = new MainAcModel();
break;
case 1:
mvpModel = new MainAcModel2();
break;
}
return this;
}
public void load(){
mvpModel.getData(new MvpModel.OnLoadCompleteListener() {
@Override
public void onLoadComplete(List<String> stringList) {
//这里可以得到数据
mvpView.showView(stringList);
}
});
}
}
在上述代码中,我们在load方法中使用匿名内部类的方式实现了MvpModel中的第二个接口。使得M和V在P中交换了数据。
而setMvpModel方法是为了证明M层和V层解耦成功而写的,与之对应的还有一个MainAcModel2:
package com.lay.mvpdemobase.model;
import java.util.ArrayList;
import java.util.List;
public class MainAcModel2 implements MvpModel {
@Override
public void getData(OnLoadCompleteListener onLoadCompleteListener) {
//1.得到数据
List<String> list= new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("这是更新之后的第"+i+"个数据");
}
if(onLoadCompleteListener!=null){
onLoadCompleteListener.onLoadComplete(list);
}
}
}
此时,稍微修改一下View层MainActivity的代码,一个MVP的简单例子就此诞生了。
package com.lay.mvpdemobase.view.activity;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.lay.mvpdemobase.R;
import com.lay.mvpdemobase.presenter.MainAcPresenter;
import com.lay.mvpdemobase.view.iview.MvpView;
import java.util.List;
/**
* 在MVP的架构中,Activity是属于View的,所以View负责展示View和更新View
*/
public class MainActivity extends Activity implements MvpView {
private ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//fbc之后的快捷键alt+enter是设置局部变量;alt+ctrl+F是成员变量
listView = (ListView) findViewById(R.id.main_lv);
// new MainAcPresenter(this).load();//此时,MVP各司其职,为了明确一下他的解耦确实做到了多写一步
new MainAcPresenter(this).setMvpModel(1).load();
}
/**
* 这个方法是更新展示View
* @param list
*/
@Override
public void showView(List list) {
//Arrayadapter的几个参数?
ArrayAdapter arrayAdapter=new ArrayAdapter(this,android.R.layout.simple_list_item_1,list);
listView.setAdapter(arrayAdapter);
}
}
运行情况如下图:
当setMvpModel(0)时,
当setMvpModel(1)时,
当数据发生改变之后,我们并不需要去改变View部分的代码,这证明我们解耦成功了。
这就是Mvp的一个简单例子,下面我们来找找他的缺点,看看我们还能怎么再改进他一些。
-
关键Bug的处理以及一些改进地方的想法。
首先,先给大家普及一个知识点,这个知识点叫做内存泄漏。
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
在刚才的那个简单例子中,我们做到了MVP的简单实现,但是他还是有一些设计上的可能会出现Bug的地方。
主要有两个,一个是因为现实项目中我们得到数据主要是从网上异步下载来的数据,那么M得到数据源就需要时间,如果在这段时间用户等不及离开了当前的Activity,就会导致内存泄漏,原因是在Presenter部分的MainAcPresenter中有他的引用变量,所以,即使Activity被销毁了,也不会被Java的垃圾回收机制回收。因为对象的创建需要消耗更多的内存,所以只有对象没有引用变量指向他的时候他才能被回收。
这就会导致这个Activity所占据的堆空间无法被释放,从而造成内存泄漏。
这个问题是在P和V的部分产生的,所以,此次改动和M部分无关。
解决方法:
使用弱引用来解决P对于V的引用问题;
知识点补充:
Android中的WeakReference 弱引用 - jamesK4W - 博客园 https://www.cnblogs.com/CVstyle/p/6395745.html
步骤一:
因为我们是要用弱引用来解决P对V的引用过程中可能产生的内存泄漏问题。所以我们首先要把对V的引用抽离出来,写一个BasePresenter来专门做这件事。
BasePresenter具体如下:
package com.lay.mvpdemobase.presenter;
import com.lay.mvpdemobase.view.iview.MvpView;
import java.lang.ref.WeakReference;
/**
* 这个地方将把P和V进行连接
*/
public class BasePresenter<V extends MvpView> {
private WeakReference<V> weakReference;
public void attch(V mvpView){
weakReference=new WeakReference<V>(mvpView);
}
public void deattch(){
if (weakReference != null) {
weakReference.clear();
weakReference=null;
}
}
public V getView(){
return weakReference.get();
}
}
而要做成这件事,还需要V部分配合,需要在对应的生命周期执行P中对应的方法;故BaseActivity如下:
package com.lay.mvpdemobase.view.activity;
import android.app.Activity;
import android.os.Bundle;
import com.lay.mvpdemobase.presenter.BasePresenter;
import com.lay.mvpdemobase.view.iview.MvpView;
public abstract class BaseActivity<V extends MvpView,T extends BasePresenter<V>> extends Activity {
public T basePresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
basePresenter=getBasePresenter();
basePresenter.attch((V)this);
}
@Override
protected void onDestroy() {
super.onDestroy();
basePresenter.deattch();
}
public abstract T getBasePresenter();
}
注意:由于我们在不同的Activity中会具体使用不同Presenter.所以我们将getBasePresenter()方法设为抽象方法,将在具体的子类中实现。
以上述简单的例子为例:
MainAcPrsenter将改为如下所示:
package com.lay.mvpdemobase.presenter;
import com.lay.mvpdemobase.model.MainAcModel;
import com.lay.mvpdemobase.model.MainAcModel2;
import com.lay.mvpdemobase.model.MvpModel;
import com.lay.mvpdemobase.view.iview.MvpView;
import java.util.List;
public class MainAcPresenter extends BasePresenter<MvpView>{
private MvpModel mvpModel;
// private MvpView mvpView; //此时已经被BasePersenter接管了这个工作
// public MainAcPresenter(MvpView mvpView) {
//
// this.mvpView = mvpView;
// }
public MainAcPresenter() {
mvpModel=new MainAcModel();
}
//直接写一个setModel方法就可以了
public MainAcPresenter setMvpModel(int model){
switch (model){
case 0:
mvpModel = new MainAcModel();
break;
case 1:
mvpModel = new MainAcModel2();
break;
}
return this;
}
public void load(){
mvpModel.getData(new MvpModel.OnLoadCompleteListener() {
@Override
public void onLoadComplete(List<String> stringList) {
//这里可以得到数据
getView().showView(stringList);
}
});
}
}
MainActivity如下:
package com.lay.mvpdemobase.view.activity;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.lay.mvpdemobase.R;
import com.lay.mvpdemobase.presenter.BasePresenter;
import com.lay.mvpdemobase.presenter.MainAcPresenter;
import com.lay.mvpdemobase.view.iview.MvpView;
import java.util.List;
/**
* 在MVP的架构中,Activity是属于View的,所以View负责展示View和更新View
*/
public class MainActivity extends BaseActivity<MvpView,MainAcPresenter> implements MvpView{
private ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//fbc之后的快捷键alt+enter是设置局部变量;alt+ctrl+F
listView = (ListView) findViewById(R.id.main_lv);
// new MainAcPresenter(this).load();//此时,MVP各司其职,为了明确一下他的解耦确实做到了多写一步
// new MainAcPresenter(this).setMvpModel(1).load();
basePresenter.setMvpModel(0).load();
}
@Override
public MainAcPresenter getBasePresenter() {
return new MainAcPresenter();
}
/**
* 这个方法是更新展示View
* @param list
*/
public void showView(List list) {
//Arrayadapter的几个参数?
ArrayAdapter arrayAdapter=new ArrayAdapter(this,android.R.layout.simple_list_item_1,list);
listView.setAdapter(arrayAdapter);
}
}
至此便解决了来自P和V之间的内存泄漏问题。
另一个问题是空指针异常。
具体原因如下:
由于jvm会适时回收view层,但是如果presenter层还在继续做耗时操作的话不会马上被回收,此时如果view已经被回收,耗时操作刚好完成要通知view层做更新ui的操作,那么就会出现空指针/空对象的异常;
比如:获取网络数据(耗时操作)成功后,需要展示到ui上,此时会调用view.showData(result)方法,view对象为空,就抛出异常了;
解决方法非常简单:
只要在具体的BasePresenter子类中,使用到View的时候,就进行一次判断是否为空就可以了。
希望对大家有所帮助!