在项目中有使用到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)
}