addFragment崩溃问题排查

问题描述

java.lang.IllegalStateException: Fragment already added: YSDialog{8c3fa2f} (fb003d68-fbff-4354-94c8-f3fc8d6a2dd4 tag=delete_dialog)
    at androidx.fragment.app.FragmentStore.addFragment(FragmentStore.java:93)
    at androidx.fragment.app.FragmentManager.addFragment(FragmentManager.java:1503)
    at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:387)
    at androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2009)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1895)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1845)
    at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1782)
    at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:565)
    at android.os.Handler.handleCallback(Handler.java:991)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loopOnce(Looper.java:232)
    at android.os.Looper.loop(Looper.java:317)
    at android.app.ActivityThread.main(ActivityThread.java:8934)
    at java.lang.reflect.Method.invoke(Method.java)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)

背景是项目中有一些崩溃,如上。stackoverflow上的相关处理如下

https://stackoverflow.com/a/55376646
https://stackoverflow.com/a/60184867

if(dialogFragment != null && dialogFragment.isAdded()) { return; }

看项目代码,以前维护的同事已经有做了处理,如上处理。

原因

DialogFragment.show 会调用 BackStackRecord.commitInternal 把commit任务post到下个looper执行。
如果连续两个commit的话,就是连续两个post,当前的isAdded的检查就无法生效了。
如下代码:

androidx.fragment.app.DialogFragment

/**
    * Display the dialog, adding the fragment to the given FragmentManager.  This
    * is a convenience for explicitly creating a transaction, adding the
    * fragment to it with the given tag, and {@link FragmentTransaction#commit() committing} it.
    * This does <em>not</em> add the transaction to the fragment back stack.  When the fragment
    * is dismissed, a new transaction will be executed to remove it from
    * the activity.
    * @param manager The FragmentManager this fragment will be added to.
    * @param tag The tag for this fragment, as per
    * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
    */
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
    mDismissed = false;
    mShownByMe = true;
    FragmentTransaction ft = manager.beginTransaction();
    ft.setReorderingAllowed(true);
    ft.add(this, tag);
    ft.commit();
}

androidx.fragment.app.BackStackRecord

@Override
public int commit() {
    return commitInternal(false);
}

int commitInternal(boolean allowStateLoss) {
    if (mCommitted) throw new IllegalStateException("commit already called");
    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
        Log.v(TAG, "Commit: " + this);
        LogWriter logw = new LogWriter(TAG);
        PrintWriter pw = new PrintWriter(logw);
        dump("  ", pw);
        pw.close();
    }
    mCommitted = true;
    if (mAddToBackStack) {
        mIndex = mManager.allocBackStackIndex();
    } else {
        mIndex = -1;
    }
    mManager.enqueueAction(this, allowStateLoss);
    return mIndex;
}

/**
 * Schedules the execution when one hasn't been scheduled already. This should happen
 * the first time {@link #enqueueAction(OpGenerator, boolean)} is called or when
 * a postponed transaction has been started with
 * {@link Fragment#startPostponedEnterTransition()}
 */
@SuppressWarnings("WeakerAccess") /* synthetic access */
void scheduleCommit() {
    synchronized (mPendingActions) {
        boolean pendingReady = mPendingActions.size() == 1;
        if (pendingReady) {
            mHost.getHandler().removeCallbacks(mExecCommit);
            mHost.getHandler().post(mExecCommit);
            updateOnBackPressedCallbackEnabled();
        }
    }
}

private Runnable mExecCommit = new Runnable() {
    @Override
    public void run() {
        execPendingActions(true);
    }
};

/**
 * Only call from main thread!
 */
boolean execPendingActions(boolean allowStateLoss) {
    ensureExecReady(allowStateLoss);

    boolean didSomething = false;
    while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
        mExecutingActions = true;
        try {
            removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
        } finally {
            cleanupExec();
        }
        didSomething = true;
    }

    updateOnBackPressedCallbackEnabled();
    doPendingDeferredStart();
    mFragmentStore.burpActive();

    return didSomething;
}

解决方法

  1. 从源头处理,唤起弹窗的位置,加下防抖代码 (治标)

  2. 针对问题处理,修改show为showNow (治本)

https://developer.android.com/reference/androidx/fragment/app/DialogFragment#showNow(androidx.fragment.app.FragmentManager,java.lang.String)

其他

DialogFragment 的 show()showNow() 的区别及使用注意事项

区别

  1. 事务提交方式

    • show():通过 FragmentTransaction.commit() 异步提交事务,将对话框的添加操作加入主线程队列,稍后执行。
    • showNow():通过 FragmentTransaction.commitNow() 同步提交事务,立即执行对话框的添加操作。
  2. 生命周期兼容性

    • show() 可安全用于 onPostResume() 之后(如按钮点击事件),但不能在 onSaveInstanceState() 调用后使用(可能导致状态不一致)。
    • showNow() 必须确保在 Activity 的 onSaveInstanceState() 之前调用,否则会抛出 IllegalStateException
  3. 回退栈处理

    • 两者默认均不将事务加入回退栈,但通过 show() 提交的事务可手动调用 addToBackStack() 实现回退逻辑;showNow() 由于同步特性,不支持此操作。

使用注意事项

  1. 生命周期时机

    • 避免在 onSaveInstanceState() 之后调用 showNow(),此时 Activity 可能已销毁,事务无法提交。
    • show() 需确保在 Activity 前台时调用,否则对话框可能无法显示(如 onCreate() 中需通过 post() 延迟到界面就绪后)。
  2. 异步与同步的依赖

    • 若后续代码依赖对话框已显示(如获取对话框视图),showNow() 更可靠;show() 需通过 onStart() 回调或 post() 确保执行顺序。
  3. 状态保存与恢复

    • 对话框状态(如输入内容)需通过 onSaveInstanceState() 自行保存,系统不会自动处理。
  4. 避免重复提交

    • 使用唯一 tag 标识对话框,防止重复调用 show()showNow() 导致多个实例重叠。
  5. 资源释放

    • onDestroy() 中调用 dismiss() 或检查 isShowing(),避免内存泄漏。

示例代码

// 使用 show()
dialogFragment.show(getSupportFragmentManager(), "dialog_tag");

// 使用 showNow()(需确保在 onSaveInstanceState 前调用)
if (!isStateSaved()) { // 检查 Activity 状态是否已保存
    dialogFragment.showNow(getSupportFragmentManager(), "dialog_tag");
}

总结

  • showNow():适合需立即显示且确定生命周期安全的场景(如 onCreate() 或用户主动触发的操作)。
  • show():更通用,适合大多数异步场景,但需注意生命周期和状态一致性。
  • 关键原则:避免在可能销毁的状态下提交事务,优先通过 isStateSaved() 检查状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值