二十九、MVP-P层优化

概述

MVP架构中,P层用于执行耗时操作,并执行UI刷新操作,可以说它负责了所有的业务场景下的UI更新逻辑。

比如,请求某个网络数据,并且根据拿到的结果,执行成功或者失败等UI刷新。

那么就存在一种情况, 正在执行操作时,用户按下返回键,当前Activity结束。此时,P层正在执行耗时操作,根据JVM的特性,正在执行中的代码属于GCRoot,此时,相当于GCRoot持有了Activity,这是Activity造成内存泄漏的典型情况。

要解决这个问题,就应该让 P层的生命周期与Activity的生命周期保持一致。Activity要回收时,P层就要释放对Activity的持有。

Lifecycle组件

AppCompatActivity 自带lifecycle特性,可以通过下面的代码打印出Activity的生命周期状态的变化。

以下是 这个Lifecycle支持的生命周期函数。

比如打开一个Activity,打印如下:

Lifecycle注解

除了上述直接添加 observer方式之外,Lifecycle组件还支持 注解的方式来监听Activity生命周期。

比如:

java 复制代码
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;

public interface IPresenter extends LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    void onCreate();

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    void onStart();

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    void onResume();

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    void onPause();

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    void onStop();

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    void onDestroy();

}

这是所有Presenter实现类都应该遵循的标准,所以让BasePresenter实现这个接口。

java 复制代码
public class BasePresenter implements IPresenter {

    private static final String TAG = BasePresenter.class.getSimpleName();

    @Override
    public void onCreate() {
        Log.d(TAG,"onCreate");
    }

    @Override
    public void onStart() {
        Log.d(TAG,"onStart");
    }

    @Override
    public void onResume() {
        Log.d(TAG,"onResume");
    }

    @Override
    public void onPause() {
        Log.d(TAG,"onPause");
    }

    @Override
    public void onStop() {
        Log.d(TAG,"onStop");
    }

    @Override
    public void onDestroy() {
        Log.d(TAG,"onDestroy");
    }
}

最后在Activity中,让 Presenter与Activity生命周期同步。

java 复制代码
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getLifecycle().addObserver(new BasePresenter());
    }
}

举一个实际的例子

上一节只是演示Lifecycle的基本使用方法,实际开发中,可能用不同步这么多生命周期方法,更多的可能是只需要其中的 onStart或者onDestory

比如,一个登录页面,点击登录按钮之后,网络请求正在执行,此时,用户要求退出登录按钮,返回游客模式。那么,在网络请求执行完毕之前,Activity得不到释放,造成泄露。

下面是 MVP架构的关键代码:

约束V层的接口 IView,目前只同一个刷新UI的方法:

java 复制代码
public interface IView {
    void refreshUI();
}

约束P层的接口IPresenter ,让所有P层方法与 Activity生命周期同步

java 复制代码
public interface IPresenter extends LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    void onCreate();

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    void onStart();

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    void onResume();

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    void onPause();

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    void onStop();

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    void onDestroy();

}

P层的基类,创建一个线程池,负责执行所有耗时请求:

java 复制代码
public abstract class BasePresenter implements IPresenter {

    private static final String TAG = BasePresenter.class.getSimpleName();


    protected ThreadPoolExecutor threadPoolExecutor;

    @Override
    public void onStart() {
        Log.d(TAG, "onStart");
    }

    @Override
    public void onResume() {
        Log.d(TAG, "onResume");
    }

    @Override
    public void onPause() {
        Log.d(TAG, "onPause");
    }

    @Override
    public void onStop() {
        Log.d(TAG, "onStop");
    }


    @Override
    public void onCreate() {
        Log.d(TAG, "onCreate 创建线程持");
        threadPoolExecutor = new ThreadPoolExecutor(
                1,
                1,
                1,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<>(3));
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy P层action全部取消");
        if (threadPoolExecutor != null) {
            threadPoolExecutor.shutdownNow();
        }
    }
}

真实的登录P类 LoginPresenter:

java 复制代码
public class LoginPresenter extends BasePresenter {

    IView iView;

    public LoginPresenter(IView iView) {
        this.iView = iView;
    }

    public void loginAction() {

        threadPoolExecutor.execute(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            iView.refreshUI();
        });
    }
}

登录的页面 LoginActivity:

java 复制代码
public class LoginActivity extends AppCompatActivity implements IView {

    ActivityLoginBinding binding;

    LoginPresenter loginPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityLoginBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        loginPresenter = new LoginPresenter(this);
        getLifecycle().addObserver(loginPresenter);
        binding.btnLogin.setOnClickListener(view -> {
            Log.d("BasePresenter", "btnLogin click at: " + System.currentTimeMillis());
            loginPresenter.loginAction();
        });

    }

    @Override
    public void refreshUI() {
        Log.d("BasePresenter", "refreshUI at :" + System.currentTimeMillis());
    }
}

在上面的代码中,我们模拟了一个登录功能,按下登录按钮之后,执行一个10秒的耗时操作,10秒之后执行UI的刷新动作。

在打开Login页面时。日志为:

bash 复制代码
2023-12-03 17:04:50.699 11388-11388/com.example.myapplication D/BasePresenter: onCreate 创建线程持
2023-12-03 17:04:50.704 11388-11388/com.example.myapplication D/BasePresenter: onStart
2023-12-03 17:04:50.705 11388-11388/com.example.myapplication D/BasePresenter: onResume

按下登录按钮时:

bash 复制代码
2023-12-03 17:05:02.141 11388-11388/com.example.myapplication D/BasePresenter: btnLogin click at: 1701594302141
2023-12-03 17:05:12.157 11388-11415/com.example.myapplication D/BasePresenter: refreshUI at :1701594312157

退出 LoginActivity时,无论登录10秒的任务是否完成,都会取消任务:

bash 复制代码
2023-12-03 17:11:27.856 11388-11388/com.example.myapplication D/BasePresenter: onStop
2023-12-03 17:11:27.858 11388-11388/com.example.myapplication D/BasePresenter: onDestroy P层action全部取消

MVP使用的误区

并不是Activity的所有生命周期函数都要同步到 P层。

比如以下场景:

有一个页面TrackActivity,负责实时刷新用户当前位置,但是为了节省手机电量,要求在页面不在前台时,停止GPS实时获取位置。

假如这是一个获取当前位置的工具类:

java 复制代码
/**
 * 获取手机当前位置
 */
public class TrackUtil {

    public void startTrack() {
        Log.d("TrackUtil","开始监听位置");
    }

    public void stopTrack() {
        Log.d("TrackUtil","停止监听位置");
    }
}

那么按照生命周期同步的原则,就应该在 Activity resume之后,开始监听,pause之后,停止监听,如下:

java 复制代码
public class TrackPresenter extends BasePresenter {

    TrackUtil trackUtil;

    IView iView;

    public TrackPresenter(IView iView) {
        this.iView = iView;

        trackUtil = new TrackUtil();
    }

    @Override
    public void onResume() {
        super.onResume();

        trackUtil.startTrack(); // 开始监听
    }

    @Override
    public void onPause() { 
        super.onPause();

        trackUtil.stopTrack(); // 停止监听
    }

}

这种写法看上去没问题,但是仔细想来,P层负责执行耗时任务以及触发刷新UI的动作,TrackUtil是否开启,关键看 Activity的生命周期,透过P层来间接开启和关闭定位,看上去有点绕。 直接把开启和关闭的逻辑放在 Activity中更合理。而且也能让 P层的功能更加纯粹。

于是改一下:

在Activity的onResume和onPause中对追踪的工具类进行开启和关闭:

java 复制代码
public class TrackActivity extends AppCompatActivity implements IView {

    ActivityTrackBinding binding;

    TrackPresenter trackPresenter;

    TrackUtil trackUtil;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityTrackBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        trackPresenter = new TrackPresenter(this);
        getLifecycle().addObserver(trackPresenter);

        binding.tvStartTrack.setOnClickListener(view -> trackPresenter.syncCurrentLocation());

    }

    @Override
    public void onResume() {
        super.onResume();
        trackUtil.startTrack();
    }

    @Override
    public void onPause() {
        super.onPause();
        trackUtil.stopTrack();
    }


    @Override
    public void refreshUI() {
        Log.d("BasePresenter", "refreshUI at :" + System.currentTimeMillis());
        binding.tvCurrentLocation.setText(trackUtil.getCurrentLocation());
    }
}

P层中,只进行刷新操作:

java 复制代码
public class TrackPresenter extends BasePresenter {

    IView iView;


    public TrackPresenter(IView iView) {
        this.iView = iView;
    }

    public void syncCurrentLocation() {
        threadPoolExecutor.execute(() -> {
            while (true) { // 模拟无限循环执行同步当前位置并显示到UI上
                iView.refreshUI();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        });

    }

}

最后补充TrackUtil:

java 复制代码
/**
 * 获取手机当前位置
 */
public class TrackUtil {

    public void startTrack() {
        Log.d("TrackUtil", "开始监听位置");
    }

    public void stopTrack() {
        Log.d("TrackUtil", "停止监听位置");
    }

    public String getCurrentLocation() {
        return "当前位置:[经度,纬度]";
    }
}

总结

本文,对MVP做出了两点优化建议:

  1. 针对P层造成的内存泄露问题,使用Lifecycle组件结合线程池来解决,Lifecycle 用来同步P层与Activity的生命周期,并在合适的时机取消线程池的任务。
  2. 为了避免P层过度臃肿,把不需要放在P层的代码挪到Activity中,保证P层代码纯正,保证功能单一原则。
相关推荐
试行38 分钟前
Android实现自定义下拉列表绑定数据
android·java
Dingdangr5 小时前
Android中的Intent的作用
android
技术无疆6 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入
GEEKVIP6 小时前
Android 恢复挑战和解决方案:如何从 Android 设备恢复删除的文件
android·笔记·安全·macos·智能手机·电脑·笔记本电脑
Jouzzy12 小时前
【Android安全】Ubuntu 16.04安装GDB和GEF
android·ubuntu·gdb
极客先躯13 小时前
java和kotlin 可以同时运行吗
android·java·开发语言·kotlin·同时运行
Good_tea_h15 小时前
Android中的单例模式
android·单例模式
计算机源码社20 小时前
分享一个基于微信小程序的居家养老服务小程序 养老服务预约安卓app uniapp(源码、调试、LW、开题、PPT)
android·微信小程序·uni-app·毕业设计项目·毕业设计源码·计算机课程设计·计算机毕业设计开题
丶白泽21 小时前
重修设计模式-结构型-门面模式
android
晨春计1 天前
【git】
android·linux·git