问题:
我们在使用Fragment的时候,会偶尔出现错误:
IllegalStateException: Can not perform this action after onSaveInstanceState,如下图:
解决方案:
根据异常信息Can not perform this action after onSaveInstanceState,可以了解到异常原因:在onSaveInstanceState行为之后,app执行某个不能响应的行为而导致异常发生。这里是指在执行onSaveInstanceState之后再调用FragmentTransaction的commit方法导致异常的发生。
官方的资料显示使用commitAllowingStateLoss则不会导致该异常的发生。
https://developer.android.com/reference/android/app/FragmentTransaction.html#commitAllowingStateLoss()
Like commit()
but allows the commit to be executed after an activity's state is saved. This is dangerous because the commit can be lost if the activity needs to later be restored from its state, so this should only be used for cases where it is okay for the UI state to change unexpectedly on the user.
源码剖析
上面已经给出了解决方案,使用commitAllowingStateLoss即可。下面从源码角度分析一下为什么。
1.首先看看FragmentTransaction的两个方法:
public abstract int commit();
public abstract int commitAllowingStateLoss();
2.FragmentTransaction中方法的实现:
@Overridepublic
int commit() {
return commitInternal(false);
}
@Override
public int commitAllowingStateLoss() {
return commitInternal(true);
}
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);
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
可以看到当我们调用commit 或者 commitAllowingStateLoss方法的时候,都会调用commitInternal(boolean allowStateLoss)这个方法,只是传参不同而已,而在commitInternal方法中最终都会调用FragmentManager种的enqueueAction方法:
public void enqueueAction(Runnable action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<Runnable>();
}
mPendingActions.add(action);
if (mPendingActions.size() == 1) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
}
}
}
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);
}
}
看到这里大家都明白了,当我们调用commit的时候,allowStateLoss为false,这个时候会调用checkStateLoss,而当Activity调用了onSaveInstanceState后,mStateSaved为True,所以会导致抛出异常。onSaveInstanceState方法是在该Activity即将被销毁前调用,来保存Activity数据的,如果在保存完状态后再给它添加Fragment就会出错。解决办法就是把commit()方法替换成 commitAllowingStateLoss()就行了,其效果是一样的。
DialogFragment怎么办?
然而在DialogFrament中,当调用show方法的时候,其内部实现为:
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
内部的实现为commit方式,并无commitAllowingStateLoss方式可以选择,可能因为有一些其他风险,Android的开发团队没有为我们提供这个方法。
解决方案:
1、try catch
@Override
public void show(FragmentManager manager, String tag) {
try{
super.show(manager,tag);
}catch (IllegalStateException ignore){
}
}
2、当然使用try catch的方式是比较欠考虑的,可以重写DialogFragment,使用反射的方式提供showAllowingStateLoss方法:
public void showAllowingStateLoss(FragmentManager manager, String tag){
try {
Field dismissed = DialogFragment.class.getDeclaredField("mDismissed");
dismissed.setAccessible(true);
dismissed.set(this, false);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
try {
Field shown = DialogFragment.class.getDeclaredField("mShownByMe");
shown.setAccessible(true);
shown.set(this, true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commitAllowingStateLoss();
}