二十九、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层代码纯正,保证功能单一原则。
相关推荐
JasonYin~30 分钟前
HarmonyOS NEXT 实战之元服务:静态案例效果---手机查看电量
android·华为·harmonyos
zhangphil39 分钟前
Android adb查看某个进程的总线程数
android·adb
抛空1 小时前
Android14 - SystemServer进程的启动与工作流程分析
android
Gerry_Liang3 小时前
记一次 Android 高内存排查
android·性能优化·内存泄露·mat
天天打码4 小时前
ThinkPHP项目如何关闭runtime下Log日志文件记录
android·java·javascript
爱数学的程序猿7 小时前
Python入门:6.深入解析Python中的序列
android·服务器·python
brhhh_sehe7 小时前
重生之我在异世界学编程之C语言:深入文件操作篇(下)
android·c语言·网络
zhangphil7 小时前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆形图实现,Kotlin(2)
android·kotlin
Calvin8808288 小时前
Android Studio 的革命性更新:Project Quartz 和 Gemini,开启 AI 开发新时代!
android·人工智能·android studio
敲代码敲到头发茂密9 小时前
【大语言模型】LangChain 核心模块介绍(Memorys)
android·语言模型·langchain