事情是这样的,我在项目里有个自定义的 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 。
通过这次问题解决,我知道了 消息池竟然是线程共享的 嗯,真香!