DialogFragment can not perform this action after onSaveInstanceState

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

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。这里可以找到:

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.

  
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状态的操作,就会引发异常。

/**
* 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()

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(),可以看看这里:

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了。