小心 DialogFragment 会造成内存泄漏

事情是这样的,我在项目里有个自定义的 Dialog 是继承 DialogFragment 实现的,接入 LeakCanary 后经常会提示我这个地方存在内存泄漏,定位的地方也有点奇怪,是一个布局控件上。

心想不应该啊,但既然报出来了,还是一探究竟。

内存泄漏

简单来说就是对象该销毁时没有被销毁回收,引用还被别的地方持有导致回收不掉,最后就变成了孤魂野鬼,达到一定程度就会导致 OOM 的问题了。

原因追踪

根据它报的位置我想难道是控件对象没被释放?于是我手动在 onDestoryView 里将控件 downLayout 赋值为 null,重试之后发现又报另外一个控件泄漏。显然问题不在这,这可能只是个表象。(得出这个推论花了我蛮长时间调试,经过多种猜测和尝试,大概定位在 DialogFragment 消失的时候,我复现问题的方式是,快速的打开销毁 DialogFragment)

回头在看看 LeakCanary 打出来的栈信息,指向的是 Message ???一开始我是不信的,打死都不信的,后来真香。表面上看 DialogFragment 的使用不会跟消息有什么关系啊,怎么会报 Message 的问题呢?看来需要我们看源码了。

DialogFragment 本质上是 Fragment 以 Dialog 的形式浮在 Activity 上展现,这个 Fragment 包含了一个 Dialog 对象,这个 Dialog 对象其实对我们是不可见的,因为我们不会直接操作它,重点就在这个 Dialog 上。

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        //省略部分......
        //Dialog 构造里会创建这个类,本质上是个 Handler 那肯定跟消息有关
        mListenersHandler = new ListenersHandler(this);
    }
//这个就是这个内部类,可以看到它对 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;
            }
        }
    }

这里我们简单看下 Dialog 的创建,简单来看就是创建对象以及一些属性设置,关联设置,有一个需要注意就是 ListenersHandler 类,本质上是个 Handler,看上去像是通过消息来处理 Dismiss, Show 等操作。

@Override
    public void onActivityCreated(Bundle savedInstanceState) {
        //省略部分......
        //这段很关键
        if (!mDialog.takeCancelAndDismissListeners("DialogFragment", this, this)) {
            throw new IllegalStateException(
                    "You can not set Dialog's OnCancelListener or OnDismissListener");
        }
        //省略部分......
    }

回到 DialogFragment 中,上面这个是 Fragment 的生命周期方法,这里 Dialog 对象调用了 takeCancelAndDismissListeners() 方法,并且将 DialogFragment 对象作为入参传入。

public boolean takeCancelAndDismissListeners(@Nullable String msg,
            @Nullable OnCancelListener cancel, @Nullable OnDismissListener dismiss) {
        //省略......
        //因为 DialogFragment 实现了 DialogInterface.OnCancelListener,
        //DialogInterface.OnDismissListener 那自然这里是成立的
        setOnCancelListener(cancel);
        setOnDismissListener(dismiss);
        
        //省略......
        
        return true;
    }
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;
        }
    }

DialogFragment 对象作为 Message 对象的 obj 属性被关联了起来。上面我们说弹框消失时会引起内存泄漏,我们就去看下 dismiss(),DialogFragment 最终会调用 Dialog 的 dismiss() 

@Override
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            dismissDialog();
        } else {
            mHandler.post(mDismissAction);
        }
    }
void dismissDialog() {
        //省略......
        try {
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;
            mWindow.closeAllPanels();
            onStop();
            mShowing = false;
            //这句是关键
            sendDismissMessage();
        }
    }
private void sendDismissMessage() {
        if (mDismissMessage != null) {
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mDismissMessage).sendToTarget();
        }
    }

从 dismiss 一路调用下来,执行完 onStop 后会调用 sendDismissMessage 方法,这个变量 mDismissMessage 就是之前我们设置的 Message,它的 obj 是 DialogFragment 对象。通过 obtain 方法,其实是从消息池里另取一个消息逐个属性赋值,最后消息是在 ListenersHandler 里处理,也就是回调了 DialogFragment 对象的 onDismiss 方法。

要说是 Message 的问题,简单看,Dialog 里有 Message 变量,但 Dialog 销毁时,他们也会跟着销毁。至于 sendDismissMessage 里获取的 Message,它是从池子里来的,最后也会回到池子里去,感觉好像没什么地方会一直持有引用。

后来查阅了资料 https://juejin.im/entry/5dd536726fb9a05a4a2d8bb7 非常感谢这篇文章,分析的很到位,让我知道了原来消息池是线程共享的,并且在消息队列阻塞的情况下会持有 Message 引用,这才是导致内存泄漏的原因所在

问题解决

根据这篇参考资料的建议 记一次 DialogFragment 造成的内存泄漏 我想自定义 Dialog 会相对的简单好处理一些。通过继承 Dialog 重写 setOnDismissListener(),setOnCancelListener() 方法,目的是不给 mDismissMessage 和 mCancelMessage 赋值了,这样就不会存在这个内存泄漏的问题,但带来的缺点是,DialogFragment 的 onDismiss 方法没有回调了。

后来我想了下,是否可以不通过发消息,而直接回调 onDismiss,因为发消息的方式其实也没有额外的逻辑。所以我想的是在原来发消息的位置替换成直接回调。那么在哪个地方比较合适呢?先看下源码:

void dismissDialog() {
        //省略......
        try {
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;
            mWindow.closeAllPanels();
            onStop();//这个方法不错,比较适合直接回调
            mShowing = false;
            //原本是要在这发消息的
            sendDismissMessage();
        }
    }

这里我们看中了 onStop 方法,而且可以重写,所以我们可以重写然后在调用 super 之后执行 onDismiss 的回调即可。不足的地方是,mShowing 字段的值更新被延后了。另外需要注意的是,这是基于 android-27 的源码,后续如果 Dialog 源码有较大变动,可能就不太合适了

cancel 操作和 dismiss 操作类似,可以重写 cancel() 方法,但是要注意,要先直接回调,再调用 super 。

通过这次问题解决,我知道了 消息池竟然是线程共享的 嗯,真香!
 

 

你可能感兴趣的:(开发过程中的问题解决,Android开发)