DialogFragment can not perform this action after onSaveInstanceState

最近在Fibric上面监控到了这样的一个Bug,可以看到是调用DialogFragment.show()报的异常。

1
2
3
4
5
6
7
Caused by java.lang.IllegalStateException
Can not perform this action after onSaveInstanceState
android.support.v4.app.FragmentManagerImpl.checkStateLoss (FragmentManager.java:2053)
android.support.v4.app.FragmentManagerImpl.enqueueAction (FragmentManager.java:2079)
android.support.v4.app.BackStackRecord.commitInternal (BackStackRecord.java:678)
android.support.v4.app.BackStackRecord.commit (BackStackRecord.java:632)
android.support.v4.app.DialogFragment.show (DialogFragment.java:143)

像这样有比较详细的堆栈信息,我们就可以从这里入手去查看一下source code。这里可以找到:

1
2
3
4
5
6
7
8
9
10
11
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);
}
}

mStateSaved等于true的时候,会报出我们监控到的异常。那么在什么时候去修改了这个变量呢?继续search code,发现只有在这里才会将这个状态置为true.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  
Parcelable saveAllState() {
···

if (HONEYCOMB) {
// As of Honeycomb, we save state after pausing. Prior to that
// it is before pausing. With fragments this is an issue, since
// there are many things you may do after pausing but before
// stopping that change the fragment state. For those older
// devices, we will not at this point say that we have saved
// the state, so we will allow them to continue doing fragment
// transactions. This retains the same semantics as Honeycomb,
// though you do have the risk of losing the very most recent state
// if the process is killed... we'll live with that.
mStateSaved = true;
}
···
}

这里说的意思是

在Honeycomb之后,会在fragment pause之后去save下当前的state。而在之前的版本保存状态是放在pause之后的。对于Fragment来说,如果我们在pause之后做了很多操作,并且在stop之前改变了fragment的状态的话,就会出现异常。对于比较旧的设备来说,在这一点上我们并没有去保存这个状态,所以我们允许他们继续处理fragment transaction。虽然这会增加当进程被杀死时丢失当前的状态,但是为了保持一样的语法风格,我们还是保留了它。

也就是说,如果当前的Fragment会采用这样的流程:onPause()->onSaveInstanceState()->onStop(),因此如果在保存状态的时候并且在onStop之前改变了Fragment的状态就会引发IllegalStateException。FragmentActivity也只有在onSaveInstanceState()的时候会保存所有的状态,如果在这个时候,如果去做Fragment状态的操作,就会引发异常。

1
2
3
4
5
6
7
8
9
10
11
/**
* Save all appropriate fragment state.
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
}

onSaveInstanceState调用的机制在Activity中很明确,在Fragment里面就稍显得复杂,我得再花点时间处理处理。

顺着上面说的,我们继续看看哪里调用了checkStateLoss()

1
2
3
4
5
6
public  void enqueueAction(Runnable action,  boolean allowStateLoss)  {
if (!allowStateLoss) {
checkStateLoss();
}
···
}

发现在这里我们会去传递一个变量allowStateLoss来控制是否允许丢失当前的状态。在FragmentManager中调用enqueueAction()的地方只有三个,分别是popBackStack()的三个重载方法,而且这三个都是用来处理从堆栈里面pop的,当然都需要在从堆栈pop的时候去检查一下当前的状态是否丢失。那么在什么情况下,这个allowStateLoss才能是一个false的值呢?

还记得我们都是怎么显示一个Fragment的嘛,会开启一个事务然后提交:

getFragmentSupportManager().beginTransaction().add(AFragment,”tag”)
.commit()

所以我们跟进到这里,其实提供了两个方法commit()commitAllowingStateLoss()。两者的区别在于,commit()提交的事务只能在Activity保存其状态之前,如果在这个点之后提交,会抛出异常。因为如果Activity需要重新恢复状态的时候,所有在保存状态之后的commit()都会丢失掉,进而就会引起上面我们看到的异常。而commitAllowingStateLoss()就是允许在Acitivty保存状态之后,仍然可以提交事务。

回到最初监控到的CrashLog,发生点是在DialogFragment#show(),可以看看这里:

1
2
3
4
5
public  void show(FragmentManager manager,  String tag)  {
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}

我们只需要把commit()换成commitAllowingStateLoss()就好了,重新复写一下show()即可。但是这样不是很建议,因为有一些状态变量没有处理。所以,目前我的处理方法是在外层需要show()的地方,都采用事务提交的方式处理:

getFragmentSupportManager().beginTransaction().add(AFragment,”tag”)
.commitAllowingStateLoss()

解决。

所以回过头来分析一下,我这里的用户行为就是,在Activity中我通过RxJava来获取一个异常状态如果出现的话,Handler它并且弹出一个Dialog给用户说明异常原因。但是如果用户在异常没有出现的时候,点击了Home键,使得上层Activity走到了onStop(),此时如果用户再次返回到程序中,由于上一次保存Activity的状态之后DialogFragment才commit(),自然会导致Crash了。

-------------The End-------------
请我喝一杯啤酒~