概述
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做出了两点优化建议:
- 针对P层造成的内存泄露问题,使用Lifecycle组件结合线程池来解决,Lifecycle 用来同步P层与Activity的生命周期,并在合适的时机取消线程池的任务。
- 为了避免P层过度臃肿,把不需要放在P层的代码挪到Activity中,保证P层代码纯正,保证功能单一原则。