在项目中有使用到BottomSheetDialogFragment,在测试中始终出现内存泄漏,在LeakCanary中看到有message的引用,本着百度一下就好了的心态上网查下就好了。结果发现并没有太好的解决方案。
先来分析下DialogFragment内存泄漏的原因:
在这里插入图片描述
此处可以看到是Handler持有的Message对象引起了内存泄漏,在Dialog源码中发现有一个mListenersHandler的变量,发现就是这个变量用来分发dimiss,show,cancel的事件

 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;
        }
    }
 private static final class ListenersHandler extends Handler {
        private final WeakReference<DialogInterface> 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;
            }
        }
    }

在mListenersHandler的实现中,发现onDismiss()中持有了dialog的引用,大概可以猜出是这个引用没有被释放导致的内存泄漏。

 /**
     * Dismiss this dialog, removing it from the screen. This method can be
     * invoked safely from any thread.  Note that you should not override this
     * method to do cleanup when the dialog is dismissed, instead implement
     * that in {@link #onStop}.
     */
    @Override
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            dismissDialog();
        } else {
            mHandler.post(mDismissAction);
        }
    }

    @UnsupportedAppUsage
    void dismissDialog() {
        if (mDecor == null || !mShowing) {
            return;
        }

        if (mWindow.isDestroyed()) {
            Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
            return;
        }

        try {
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;
            mWindow.closeAllPanels();
            onStop();
            mShowing = false;

            sendDismissMessage();
        }
    }

在dismiss方法中调用了sendDismissMessage()

private void sendDismissMessage() {
        if (mDismissMessage != null) {
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mDismissMessage).sendToTarget();
        }
    }

这个 mDismissMessage 持有了Dialog的引用,在dialogFragment中调用此方法传递的是dialogFragment中mDialog对象,即持有dialogFragment的引用导致的内存泄漏。

但是有点不明白,为什么会导致泄漏?
在网上看大神说是其他线程持有mDismissMessage,而mDismissMessage又持有dialog的引用导致的内存泄漏。

盗张图:
在这里插入图片描述
大体就是当Dialog关闭dismiss时,刚好取出的是已经回收的消息,并且这条消息被另一个线程所引用,此时的mDimissMessage重新引用了DialogFragment,因此不能被回收,造成内存泄露。
说的不太清楚,可以看下这篇文章分析的挺好:传送门

网上的解决方案大都是将setOnDismissListener() 置空,但是这样会有BUG;还有就是重写DialogFragment,在setOnDismissListener()传一个弱引用的DialogFragment,但比较麻烦

解决方案:
网上搜到了一个解决方案,亲测可用


/**
 * https://medium.com/square-corner-blog/a-small-leak-will-sink-a-great-ship-efbae00f9a0f
 * square 的解决方案。View detach 的时候就将引用置为 null 了,
 * 会导致 Dialog 重新显示的时候,原来设置的 Listener 收不到回调
 *
 * 在 show 之后,调用 clearOnDetach
 * */
class ClearOnDetachListener(private var delegate: DialogInterface.OnClickListener?) :
        DialogInterface.OnClickListener {
    override fun onClick(dialog: DialogInterface?, which: Int) {
        delegate?.onClick(dialog, which)
    }

    fun clearOnDetach(dialog: Dialog) {
        dialog.window?.decorView?.viewTreeObserver?.addOnWindowAttachListener(object :
                ViewTreeObserver.OnWindowAttachListener {
            override fun onWindowDetached() {
                delegate = null
            }

            override fun onWindowAttached() {
            }
        })
    }
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
        val clearOnDetachListener = ClearOnDetachListener(DialogInterface.OnClickListener { dialog, which -> })
        this.dialog?.let {
            clearOnDetachListener.clearOnDetach(it)
        }
        super.onActivityCreated(savedInstanceState)
    }
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐