记一次DialogFragment引发的内存泄漏

最近发现app中的内存泄漏问题比较多,准备这段时间将内存泄漏的问题都解决一下,发现关于DialogFragment相关的内存泄漏非常多,那就先拿它开刀吧。

 

记一次DialogFragment引发的内存泄漏_第1张图片

那就只能看源码来发现问题。既然和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是来控制是否需要做点击外部区域关闭的。

 

你可能感兴趣的:(记一次DialogFragment引发的内存泄漏)