Q:咋回事?正常使用 Dialog 和 DialogFragment 也有可能会导致内存泄漏?
A: ....是的,说来话长。
长话短说:
- 某一个 HandlerThread 的 Looper#loop 方法,一直等待 queue#next 方法返回,但是它的 msg 局部变量还引用着上一个循环中已经被放到 Message Pool 中 Message,我们称之为 MessageA。
- DialogFragment#onActivityCreated 方法中,会调用 Dialog#setOnCancelListener 方法,将自身的引用作为 listener 参数传递给该方法
- Dialog#setOnCancelListener 方法内部,会尝试从 Message Pool 中获取一个 Message,取出的 Message 刚好是 MessageA,然后将传入的 Listener 实例赋值给 MessageA#obj。
- 外部调用 cancel 的时候,Dialog 内部会将 MessageA 拷贝一份,我们称它为 MessageB,然后将 MessageB 发送到消息队列中。
- DialogFragment 收到 onDestory 回调之后,LeakCanary 开始监听这个 DialogFragment 是否正常被回收,发现这个实例一直存在,dump 内存,分析引用链,报告内存泄漏问题。
具体细节介绍见下文👇
1、问题
开发的时候, LeakCanary 报告了一个诡异的内存泄漏链。
操作路径:app 显示 DialogFragment 然后点击外部使其消失,之后 LeakCanary 就报了如下问题:
从上面的截图 👆 可以看出:GCRoot 是 HandlerThread 正在执行的方法中的一个局部变量。这个局部变量强引用了一个 Message 对象,message 的 obj 字段又强引用了 NormalDialogFragment ,导致其调用了 onDestory 方法之后,也无法被回收。
2、分析
注:本文中的「HandlerThread」泛指那些带有 Looper 并且开启了消息循环(调用了 Looper#loop)的线程
DialogFragment 为啥会被一个 Message 的 obj 字段强引用?而且那还是一个被 HandlerThread 引用着的 Message。
回顾一下我们正常显示 DialogFragment 的流程:1、实例化 DialogFragment,2、调用 DialogFragment#show 方法让其显示出来。这个流程中有可能导致 Fragment 被 Message 强引用吗?
- 首先看 DialogFragment 的构造方法是一个空实现。排除。
- 其次看 DialogFragment show 方法逻辑如 👇,也是正常的 Fragment 显示逻辑。排除。
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
难道是 show 过程的某个步骤中去获取了 Message? 在 DialogFragment#onActivityCreated 方法中,可以看到
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (!mShowsDialog) {
return;
}
//省略一些代码
mDialog.setCancelable(mCancelable);
mDialog.setOnCancelListener(this);//设置 cancel 监听器
mDialog.setOnDismissListener(this);//设置 dismiss 监听器
//省略一些代码
}
以 Dialog#setOnCancelListener 方法为例 👇
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) {
//Listener 不为 null,取出一条 message(会尝试先从 pool 中获取,如果没有消息才会 new 一个新的) 这是一个比较关键的点,后续会讲到
mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener