问题复现
最近出现了一个很奇怪的问题,问题异常日志如下:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2053)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2079)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:678)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:632)
at android.support.v4.app.DialogFragment.dismissInternal(DialogFragment.java:223)
at android.support.v4.app.DialogFragment.dismiss(DialogFragment.java:190)
刚开始以为是 DialogFragment 调用 show 的时候,没有判断 Activity 是否已经 Finish 了。结果发现在项目代码里是做了判断的,才知道肯定没这么简单。
FragmentManagerImpl.checkStateLoss
private void checkStateLoss() {
//异常抛出的地方
if (mStateSaved) {
throw new IllegalStateException("Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException("Can not perform this action inside of " + mNoTransactionsBecause);
}
}
FragmentManagerImpl. enqueueAction
/**
* Adds an action to the queue of pending actions.
*
* @param action the action to add
* @param allowStateLoss whether to allow loss of state information
* @throws IllegalStateException if the activity has been destroyed
*/
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
//注意这里的allowStateLoss 它决定了checkStateLoss方法是否进行
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
scheduleCommit();
}
}
BackStackRecord.commitInternal
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Commit: " + this);
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
dump(" ", null, pw, null);
pw.close();
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
//重点
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
BackStackRecord.commit
public int commit() {
//这里传入的就是allowStateLoss参数
return commitInternal(false);
}
DialogFragment.show
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
//没想到的时候DialogFragment的show方法会调用commit() 导火索
ft.commit();
}
目前来看是因为 DialogFragment 的 show 方法导致强行检查 Fragment 的状态,而恰好在检查的时候状态已经被保存导致 FragmentManager 的标记位 mStateSaved = true 。
情景再现
当用户在进入首页模块时直接按 Home 键或者遇到内存不足等特殊情况下触发了onSaveInstanceState 导致 FragmentManager 进行“智能”保存当前 Activity 中 Fragment 的状态,因为 DialogFragment (广告弹窗)需要先从服务器拉取数据,所以之后在 show 的时候去 checkStateLoss 时触发了异常。
解决方案
- 1、将广告弹窗实现改为纯
Dialog
,不再使用DialogFragment
; - 2如果自己的项目里面不需要对
Fragment
的相关状态进行保存和维护,可以在相关Activity
中复写onSaveInstance
不进行super
回调即可(不建议使用此方案); - 3、可重写
DialogFragment
中的show
方法,并使用commitAllowStateLoss
提交,如下代码(推荐使用此方案)。
// 方案3代码示例
override fun show(manager: FragmentManager?, tag: String?) {
// super.show(manager, tag)
val ft = manager?.beginTransaction()
ft?.add(this, tag)
ft?.commitAllowingStateLoss()
}