最近发现app中的内存泄漏问题比较多,准备这段时间将内存泄漏的问题都解决一下,发现关于DialogFragment相关的内存泄漏非常多,那就先拿它开刀吧。
那就只能看源码来发现问题。既然和Message有关,那么先要找到Message使用的地方在哪,通过源码发现在Dialog源码中有三个全局变量
private Message mCancelMessage;
private Message mDismissMessage;
private Message mShowMessage;
通过名字可以看出是和关闭、显示、消失这三个状态相关的参数,那么它们的初始化又在哪呢?
public void setOnCancelListener(@Nullable OnCancelListener listener) {
if (mCancelAndDismissTaken != null) {
throw new IllegalStateException(
"OnCancelListener is already taken by "
+ mCancelAndDismissTaken + " and can not be replaced.");
}
if (listener != null) {
mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
} else {
mCancelMessage = null;
}
}
public void setOnDismissListener(@Nullable OnDismissListener listener) {
if (mCancelAndDismissTaken != null) {
throw new IllegalStateException(
"OnDismissListener is already taken by "
+ mCancelAndDismissTaken + " and can not be replaced.");
}
if (listener != null) {
mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
} else {
mDismissMessage = null;
}
}
public void setOnShowListener(@Nullable OnShowListener listener) {
if (listener != null) {
mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
} else {
mShowMessage = null;
}
}
从代码可以看出,只要设置了它们对应的监听,就会生成相应的Message。
但是为什么会产生内存泄漏,我们还不知道呢,那到底是什么引起的内存泄漏呢?
我们知道,java的内存回收机制中有一个引用与回收问题,当一个对象有其他的对象引用的时候,回收机制就不会回收这个对象,那么如果两对象互相引用,就会出现一个死循环,谁都回收不了。在DialogFragment中,我们设置了setOnDismissListener监听,它会将DialogFragment设置进去,生成mDismissMessage对象mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);(listener就是DialogFragment),而mDismissMessage在Dialog中,Dialog又是在DialogFragment中。
这是dialog中代码
private static final class ListenersHandler extends Handler {
private final WeakReference mDialog;
public ListenersHandler(Dialog dialog) {
mDialog = new WeakReference<>(dialog);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DISMISS:
((OnDismissListener) msg.obj).onDismiss(mDialog.get());
break;
case CANCEL:
((OnCancelListener) msg.obj).onCancel(mDialog.get());
break;
case SHOW:
((OnShowListener) msg.obj).onShow(mDialog.get());
break;
}
}
}
这是DialogFragment中代码
@Override
public void onDismiss(DialogInterface dialog) {
if (!mViewDestroyed) {
// Note: we need to use allowStateLoss, because the dialog
// dispatches this asynchronously so we can receive the call
// after the activity is paused. Worst case, when the user comes
// back to the activity they see the dialog again.
dismissInternal(true);
}
}
void dismissInternal(boolean allowStateLoss) {
if (mDismissed) {
return;
}
mDismissed = true;
mShownByMe = false;
if (mDialog != null) {
mDialog.dismiss();
}
mViewDestroyed = true;
if (mBackStackId >= 0) {
getFragmentManager().popBackStack(mBackStackId,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mBackStackId = -1;
} else {
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.remove(this);
if (allowStateLoss) {
ft.commitAllowingStateLoss();
} else {
ft.commit();
}
}
}
当我们用完了mDismissMessage消息时,需要回收这个Message,发现DialogFragment还存在着,那么就需要等待DialogFragment的回收,我们看到ListenersHandler在发送消息之后才会调用onDismiss方法,这时才开始去回收DialogFragment,但是发现dialog中的mDismissMessage还没有回收,这两个对象就一直在互相等待对方的回收,就会出现谁都回收不了,就内存泄漏了,所以报的也就是Message.obj的问题了。
我们通过分析知道是设置了listener出现的问题,那么下一步我们需要找到的就是DialogFragment到底在什么地方设置这些监听。通过查找,发现在onActivityCreated设置了
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (!mShowsDialog) {
return;
}
View view = getView();
if (view != null) {
if (view.getParent() != null) {
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
mDialog.setContentView(view);
}
final Activity activity = getActivity();
if (activity != null) {
mDialog.setOwnerActivity(activity);
}
mDialog.setCancelable(mCancelable);
mDialog.setOnCancelListener(this);
mDialog.setOnDismissListener(this);
if (savedInstanceState != null) {
Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
if (dialogState != null) {
mDialog.onRestoreInstanceState(dialogState);
}
}
}
在21和22行中可以看出,确实是设置了监听。那么我们只需要将监听取消不就可以了吗。我也在网上搜索了,网上有些分析是直接置为空,但是运行之后,发现还是会出现泄漏的情况,这里也贴出这种解决方法:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getDialog() != null) {
try {
// 解决Dialog内D存泄漏
getDialog().setOnDismissListener(null);
getDialog().setOnCancelListener(null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过源码,我们可以看到,如果设置mShowsDialog为false的时候,直接就返回了,这就有操作空间,我们直接不要系统的,然后重新设置一遍代码不就行了吗?创建一个BaseDialogFragment,重写onActivityCreated方法
@Override
public void onActivityCreated(Bundle savedInstanceState) {
boolean isShow = this.getShowsDialog();
this.setShowsDialog(false);
super.onActivityCreated(savedInstanceState);
this.setShowsDialog(isShow);
View view = getView();
if (view != null)
{
if (view.getParent() != null)
{
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
this.getDialog().setContentView(view);
}
final Activity activity = getActivity();
if (activity != null)
{
this.getDialog().setOwnerActivity(activity);
}
if (savedInstanceState != null)
{
Bundle dialogState = savedInstanceState.getBundle("android:savedDialogState");
if (dialogState != null)
{
this.getDialog().onRestoreInstanceState(dialogState);
}
}
}
通过系统给的漏洞,绕过设置监听的地方,就可以了。
运行确实可以成功,但是发现了一个问题,在点击空白地区关闭弹窗时,从后台回来后前面关闭的弹窗会自动弹出,这就是个大问题啊。通过我们操作,可以猜测出应该是setOnCancelListener这个监听没有设置的原因,但是如果设置了,又会内存泄漏。那么我们直接将点击其他区域关闭的监听关掉不就行了。
.但是,对于一个app来说,我们开发说了不算,产品肯定不会同意点击其它区域不能关闭的弹窗出现。我们可以通过迂回的方式来设置啊,可以监听touchListener的方法来监听其它区域,不就可以来控制是否能关闭。还有就是需要监听返回键关闭的事件,最终的BaseDialogFragment是:
private boolean mCancelable = true;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
boolean isShow = this.getShowsDialog();
this.setShowsDialog(false);
super.onActivityCreated(savedInstanceState);
this.setShowsDialog(isShow);
View view = getView();
if (view != null)
{
if (view.getParent() != null)
{
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
this.getDialog().setContentView(view);
}
final Activity activity = getActivity();
if (activity != null)
{
this.getDialog().setOwnerActivity(activity);
}
this.getDialog().setCancelable(false);
this.getDialog().getWindow().getDecorView().setOnTouchListener(this);
this.getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE){
dismiss();
return true;
}
return false;
}
});
if (savedInstanceState != null)
{
Bundle dialogState = savedInstanceState.getBundle("android:savedDialogState");
if (dialogState != null)
{
this.getDialog().onRestoreInstanceState(dialogState);
}
}
}
public void setCancelable(boolean mCancelable) {
this.mCancelable = mCancelable;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mCancelable && getDialog().isShowing()) {
dismiss();
return true;
}
return false;
}
这样就可以解决这个问题了mCancelable是来控制是否需要做点击外部区域关闭的。