转载请注明本文出自远古大钟的博客(http://blog.csdn.net/duo2005duo),谢谢支持!
续上一篇 Android框架模式(1)-MVP入门这一篇主要介绍上一篇MVP的Demo中存在的问题,以及如何改进。 另外一方面,我还会介绍MVP实现中容易出现的问题来引出MVP中值得注意的细节。通过这篇文章,你将知道如何更好地使用MVP。(这篇文章需要有一定RxJava的基础,如果没有则跳过看最后的几个建议)
在Android中,多线程异步代码通常是不可避免的,例如View中为了保证TextView的setText在主线程中调用,在View层有这样的代码
public class View extends Activity implements IView{
@override
public void setData(String data){
//保证对View的修改运行在主线程上
runOnUiThread(new Runnable(){ //ugly
public void run(){
text.setText(data);
}
}
);
}
}
又比如在Model层中,为了避免在主线程中执行耗时操作。
public class Model implements IModel{
void getData(ICallback callback){
execute(new Runnable(){
public void run(){ //ugly
... //这里是耗时操作
callback.onResult("hello world"); //10 返回数据
}
};
}
}
以上是利用Java原有的多线程写法写的.当然,这么简单的代码用这种写法完全没有问题。但是,当真正在项目中使用时,你可能需要考虑其他几个问题:
1.错误处理。Model的数据可能是来自不可靠数据源,比如网络,那你必须添加额外的错误处理代码
2.线程切换。例如,当APP增加某一个功能后,某一个工作线程A不堪重负,将线程A中某些任务集合分配到其他线程来执行。
3.取消操作。在向Model请求数据后,但是在Model返回数据前,由于View的状态的一些变化(比如Home键Activity到后台),这时View不在需要收到这些数据,我们需要阻止这些数据传到View中。
当把上述逻辑加到原有代码后,异步代码非常臃肿。
所以,在MVP中应该如何解决这个问题呢?
1.把分配线程的职责全交给Presenter
之所以选择在Presenter中分配线程,是因为Presenter是View跟Model的中间人,能够分别从View跟Model中抽取异步逻辑代码,统一到一个类(Presenter)中,便于维护。所以,上一篇的Demo就应该调整成如下:
//Model层
public class Model implements IModel{
String getData(){ //不需要Callback,因为逻辑已经一到Presenter中
... //耗时操作
return "hello world"; //删除异步逻辑,调整到Presenter中
}
}
//View 层
public class View extends Activity implements IView{
@override
public void setData(String data){
text.setText(data); //删除异步逻辑,调整到Presenter中
}
}
//Presenter 层
public class Presenter implements IPresenter{
...
@override
public void performOnClick(){
execute(new Runnable(){
public void run(){ //ugly!!! 从Model中移来的异步代码
... //这里是耗时操作
String data=model.getData(); //不需要Callback类,直接返回
String dataFromPresenter=data+" from presenter"; //8 加工数据
runOnUiThread(new Runnable(){ //ugly!!!从View中移来的异步代码
public void run(){
view.setData(dataFromPresenter);
}
}
);
}
};
}
}
}
可以看到,将异步逻辑移动到Presenter后,变化主要有两个
1.不再需要Model.Callback类,Model的获取到数据后直接返回即可
2.View,Model层的异步代码移动到Presenter,Presenter能控制Model,View的执行线程
到此,我们已经将异步逻辑控制移动到Presenter中,这一步主要是为了下一步RxJava作的准备,目的是为了让RxJava的流水式写法不会断开。
2.使用RxJava优化Presenter中的嵌套逻辑
用RxJava实现Presenter中的代码比较简单,懂RxJava都能改成如下代码:
//Presenter 层
public class Presenter implements IPresenter{
...
@override
public void performOnClick(){
//RxJava将原本栈式的逻辑改成流式的逻辑,按顺序略读即可,当然你也可以把生成Observable得代码移动到Model中。
Observable
.create(new OnSubscribe<String>(){
public void call(Subscriber<? super String> subscriber){ //调用Model
String data=model.getData();
subscriber.onNext(data);
subscriber.onCompleted();
}
})
.subscribeOn(Schedulers.io()) //配置调用Model的线程
.observeOn(AndroidSchedulers.mainThread()) //配置调用View的线程
.subscribe(new Subscriber<String>(){
@Override
public void onCompleted(){}
@Override
public void onError(Throwable throwable){} //错误监听,当然在这里永远调用不到
@Override
public void onNect(String s){
view.setData(dataFromPresenter); //调用View
}
});
}
}
尽管上一篇文章中,我们已经按照依赖倒置原则来使用IView,IPresenter,IModel接口让MVP各层容易改变。但耦合问题依然存在。我们对View层,Presenter层来展开分析
//View层
private class ViewA implements IView{
private IPresenter mPresenter=new PresenterA(this);// 1
...
}
//Presenter
private class PresenterA implments IPresenter{
private IView mView;
public PresenterA(IView view){
mView=view;
}
...
}
对于1处,假设我们某一天不适用PresenterA,要使用PresenterB,我们只需要换上PresenterB即可,这似乎非常简单。
但是,在分层结构中,每个层都应该是独立的。Presenter层的修改完全不应该影响到View,但是在这个例子中,我们为了修改Presenter层却无奈地同时改变了View层,所以View层跟Presenter依然存在耦合,当然Presenter跟Model也存在这样的问题。
解决以上的问题需要借助依赖注入工具,目前主流的依赖注入工具是Dagger2,由Google负责维护。如果完全不认识Dagger2的话,可以看这一篇Android常用开源工具(1)-Dagger2入门
对于Android来说,就算Activity所在的进程被kill掉,Activity的状态却依然保留,除非用户主观地关闭这个Activity。Presenter,Model是Android本身不具备的,我们必须为它们添加额外代码来保证它们顺利地在进程重启的过程中维持状态。
Activity在以下的情况下系统会把Activity重启:
1. Activity 在屏幕翻转
2. Activity长时间(30分钟)后台再进入这个Activity的时候 (2.3或更旧版本)
3. 内存不足,Activity被杀死,之后打开这个Activity
在这种情况下,重启的Activity将会是一个全新的实例,并会利用savedInstanceState
恢复到重启前的状态。
Android框架给Activity提供了这种超越进程的恢复能力,而Activity又经常被我们用于MVP中V。如果我们不同时对PM做一些特殊处理的话,当Activity重启时,MVP三者状态的不协调。
为了说明这种不协调,一个简单的例子:TextView最初为0,按下载按钮,在Presenter层开启一个下载线程,每一秒从Model层中获取下载进度并在TextView中显示,然后按home键到后台,内存不足,Activity被杀死,之后重启Activity,Activity由于savedInstanceState
,进度条停留在上一次进程被杀死的位置,但是却发现进度条不动了。这是因为重启Activity,Presenter层的线程没有重新被启动。
为了解决以上的问题,我们必须把正在运行的线程绑定到一个id号,保存在savedInstanceState
,在进程重启的时候将对应id号的线程复活,在此我写大致思想,细节涉及RxJava的回收等先不写:(用到RxJava)
//大致的思想就是把View发来的事件对应到一个int类型的Id上,存Id就相当于存这个事件,在进程死亡前存这个Id,在进程重启后通过读这个Id重启这个事件。
public class Presenter implements IPresenter{
//注册一些可能重启进程后可能被重启的线程 (id,Thread)
private SparseArray<Function0<Subscription>> mRestartables=new SparseArray<>();
//运行过的线程ID
private List<Integer> mRunningIds=new ArraysList<>();
//运行过的线程
private SparseArray<Subscription> mReqeusts=new SparseArray<>();
...
public void onCreate(Bundle savedInstanceState){
//注册线程,绑定从DONWLOAD到线程的映射
mRestartables.put(DOWNLOAD_ID,new Function0<Subscription>(){
Subscription call(){
return mModel
.getDownloadObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(()->)
.subscribe(new Action1<Integer>(){
public void call(Integer progress){
mView.setProgress();
}
});
}
});
//Activity重启,可能有未完成线程可以在这里得到重启
if (savedInstanceState==null){
for (int id:savedInstanceState.getIntArrayList(KEY_RUNNINGS)){
mRestartables.get(id).call();
mRunningIds.add(id);
mReqeusts.add(id);
}
}
}
public void onSaveInstanceState(Bundle outState){
//保存未完成的线程,进程重启后能够自动重启这部分线程
for (int id:mRunningIds){
if (mRequests.get(id).isUnsubcribed()){
mRunningIds.remove((Integer)id);
}
}
outState.put(KEY_RUNNINGS,mRunningIds);
}
public void startDownload(){
//有下载任务时候,启动线程,并保留线程索引
Subscription downloadSubscription=mRestartables.get(DOWNLOAD_ID).call();
mRequests.put(DOWNLOAD_ID,downloadSubscription);
}
}
经过以上相对麻烦的过程,线程得到重启,但是又有另外一个问题:
Activity 在屏幕翻转,长时间后台再进入这个Activity的时候,Activity会reCreate,那将要手动重新开辟新的线程,不会使用之前的线程,因为无法获取到上一次启动的线程引用以及运行状态。这将导致严重的问题:一方面,如果你没有关闭之前的旧线程,那旧线程持有旧的View将导致内存泄露;另外一方面,就算能及时关闭之前的线程,线程不断地重启也是不需要,会造成资源浪费。
所以,我推荐将Presenter做成一个RetainFragment,以下形式:
//Presenter变成Retained Fragment,你将不需要再View中显式调用Presenter.onCreate();
public class Presenter extends Fragment implment IPresenter{
...
public onCreate(Bundlet bundle){
setRetainInstance(true);
}
}
public class View extends Activity implmetns IView{
private IPresenter mPresenter=new Presenter(this);
public void onCreate(Bundle b){
super.onCreate(b);
//添加Presenter,将不需要再View中显式调用Presenter.onCreate()
getFragmentManager().beginTransaction().put((Fragment)mPresenter,"Presenter").commit();
}
}
经过这样,设置发生改变(转屏等)就不会导致线程重启。
是的,不得不承认上述的解决方案很麻烦,一种框架急需出现来解决这个问题。有个老外已经写了这个框架,这个框架我没看出有什么错误的地方(之前有线程方面的bug经过我提issue这名老外已经解决)。这个框架叫Nucleus,我也在用,推荐给大家。
1.设计View的接口时候,要考虑这个接口的必要性,View的接口只是为了传输数据而不是为了让Presenter来控制,同时也要注意Presenter只从Model中获取数据,举个例子:
public interface IView{
void startSomeActivity(); //错,Presenter不能控制View,只能设置数据
void showTextView(); //错,Presenter不能控制View,只能设置数据
void setActivityPrepareData(DownloadInfo info);//对
void setViewAdapterData(List<Data> listData);//对
void showToast(int resId);//错 ResourceId不是从Model层中获取,View选取哪个ResourceId来显示是View自己的责任
}
2.Presenter通过以下两点来解耦MV两层,最好不要有其他责任
a)整理并保存线程逻辑
b)从Model获取Model层的POJO转成View层的POJO或者基本数据元素,注意前后两个POJO必须是不同的。View不能直接使用Model层的POJO
3.各层接口命名避免模糊。举个例子
public interface IPresenter{
void init(); //错 模糊不清,View层根本不知道应该什么时候调用init()
void onCreate(); //对, View层知道在onCreate的时候调用Presenter的onCreate()方法
}