Message 引发的 DialogFragment 内存泄漏分析与解决方案

本文分析了DialogFragment因Message引起的内存泄漏问题,详细解释了问题出现的原因、泄漏过程,并提供了系统侧和App侧的解决方案,包括修改Looper#loop方法、避免从消息池获取Message以及使用特定的Dialog和DialogFragment子类来切断引用链。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Q:咋回事?正常使用 Dialog 和 DialogFragment 也有可能会导致内存泄漏?

A: ....是的,说来话长。

长话短说:

  1. 某一个 HandlerThread 的 Looper#loop 方法,一直等待 queue#next 方法返回,但是它的 msg 局部变量还引用着上一个循环中已经被放到 Message Pool 中 Message,我们称之为 MessageA。
  2. DialogFragment#onActivityCreated 方法中,会调用 Dialog#setOnCancelListener 方法,将自身的引用作为 listener 参数传递给该方法
  3. Dialog#setOnCancelListener 方法内部,会尝试从 Message Pool 中获取一个 Message,取出的 Message 刚好是 MessageA,然后将传入的 Listener 实例赋值给 MessageA#obj。
  4. 外部调用 cancel 的时候,Dialog 内部会将 MessageA 拷贝一份,我们称它为 MessageB,然后将 MessageB 发送到消息队列中。
  5. DialogFragment 收到 onDestory 回调之后,LeakCanary 开始监听这个 DialogFragment 是否正常被回收,发现这个实例一直存在,dump 内存,分析引用链,报告内存泄漏问题。

具体细节介绍见下文👇

1、问题


开发的时候, LeakCanary 报告了一个诡异的内存泄漏链。

操作路径:app 显示 DialogFragment 然后点击外部使其消失,之后 LeakCanary 就报了如下问题:

 

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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值