问题描述
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;
}
解决方法
-
从源头处理,唤起弹窗的位置,加下防抖代码 (治标)
-
针对问题处理,修改show为showNow (治本)
https://developer.android.com/reference/androidx/fragment/app/DialogFragment#showNow(androidx.fragment.app.FragmentManager,java.lang.String)
其他
DialogFragment 的 show()
与 showNow()
的区别及使用注意事项
区别
-
事务提交方式
show()
:通过FragmentTransaction.commit()
异步提交事务,将对话框的添加操作加入主线程队列,稍后执行。showNow()
:通过FragmentTransaction.commitNow()
同步提交事务,立即执行对话框的添加操作。
-
生命周期兼容性
show()
可安全用于onPostResume()
之后(如按钮点击事件),但不能在onSaveInstanceState()
调用后使用(可能导致状态不一致)。showNow()
必须确保在 Activity 的onSaveInstanceState()
之前调用,否则会抛出IllegalStateException
。
-
回退栈处理
- 两者默认均不将事务加入回退栈,但通过
show()
提交的事务可手动调用addToBackStack()
实现回退逻辑;showNow()
由于同步特性,不支持此操作。
- 两者默认均不将事务加入回退栈,但通过
使用注意事项
-
生命周期时机
- 避免在
onSaveInstanceState()
之后调用showNow()
,此时 Activity 可能已销毁,事务无法提交。 show()
需确保在 Activity 前台时调用,否则对话框可能无法显示(如onCreate()
中需通过post()
延迟到界面就绪后)。
- 避免在
-
异步与同步的依赖
- 若后续代码依赖对话框已显示(如获取对话框视图),
showNow()
更可靠;show()
需通过onStart()
回调或post()
确保执行顺序。
- 若后续代码依赖对话框已显示(如获取对话框视图),
-
状态保存与恢复
- 对话框状态(如输入内容)需通过
onSaveInstanceState()
自行保存,系统不会自动处理。
- 对话框状态(如输入内容)需通过
-
避免重复提交
- 使用唯一
tag
标识对话框,防止重复调用show()
或showNow()
导致多个实例重叠。
- 使用唯一
-
资源释放
- 在
onDestroy()
中调用dismiss()
或检查isShowing()
,避免内存泄漏。
- 在
示例代码
// 使用 show()
dialogFragment.show(getSupportFragmentManager(), "dialog_tag");
// 使用 showNow()(需确保在 onSaveInstanceState 前调用)
if (!isStateSaved()) { // 检查 Activity 状态是否已保存
dialogFragment.showNow(getSupportFragmentManager(), "dialog_tag");
}
总结
showNow()
:适合需立即显示且确定生命周期安全的场景(如onCreate()
或用户主动触发的操作)。show()
:更通用,适合大多数异步场景,但需注意生命周期和状态一致性。- 关键原则:避免在可能销毁的状态下提交事务,优先通过
isStateSaved()
检查状态。