最近查看Bugly错误统计时,看到这样一个异常信息:java.lang.IllegalStateException:
Can not perform this action after onSaveInstanceState,代码定位:transaction.commit()这一行,现做个问题总结,希望能帮到遇到同问题的朋友。
一、异常原因
查看下源码
@Override
public int commit() {
return commitInternal(false);
}
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(" ", pw);
pw.close();
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
if (allowStateLoss) {
// This FragmentManager isn't attached, so drop the entire transaction.
return;
}
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
scheduleCommit();
}
}
private void checkStateLoss() {
if (isStateSaved()) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
}
@Override
public boolean isStateSaved() {
// See saveAllState() for the explanation of this. We do this for
// all platform versions, to keep our behavior more consistent between
// them.
return mStateSaved || mStopped;
}
追踪 transaction.commit()方法,最终BackStackRecord 类调用FragmentManagerImpl的checkStateLoss(),判断mStateSaved或者mStopped为true,就会抛出这个异常信息,意思是说 在onSaveInstanceState之后或者onStop之后不能再执行此操作。
二、异常分析
1、 transaction.commit()操作
/**
* Schedules a commit of this transaction. The commit does
* not happen immediately; it will be scheduled as work on the main thread
* to be done the next time that thread is ready.
*
* <p class="note">A transaction can only be committed with this method
* prior to its containing activity saving its state. If the commit is
* attempted after that point, an exception will be thrown. This is
* because the state after the commit can be lost if the activity needs to
* be restored from its state. See {@link #commitAllowingStateLoss()} for
* situations where it may be okay to lose the commit.</p>
*
* @return Returns the identifier of this transaction's back stack entry,
* if {@link #addToBackStack(String)} had been called. Otherwise, returns
* a negative number.
*/
public abstract int commit();
源码说明大概意思:commit transaction;不会立刻执行,它会被调度为主线程上的工作,下次线程准备好时再执行;使用此方法提交事务,只能在其activity保存状态之前,如果提交事务在其activity保存状态之后就会引发异常,因为commit之后的状态会丢失,如果activity需要restored状态。使用commitAllowingStateLoss() 方法提交事务 状态丢失是可以的;返回此事务的堆栈项的标识符,如果调用了addToBackStack(String)。否则,返回一个负数。
那么如果不考虑状态的丢失,提交事务 改成commitAllowingStateLoss(),就不会有这个异常信息,但是如果activity恢复,那么commit 的将会丢失,这肯定不行。
这里可以看到commit是不会立刻执行的,但是源码里有commitNow()方法,这个方法是可以即刻执行的,但是这个方法不支持addToBackStack(String),看具体需求使用了,同样commitNow()也不可以在activity 保存状态之后使用,commitNowAllowingStateLoss()方法可以,但是它同样是危险的,会丢失commit。
一般情况下,transaction的add、show、hide等行为 commit时都是与用户交互后产生的,我这里的commit是存在异步网络请求之后,根据后端数据确定add某个Fragment或者show、hide某个Fragment,这里就有可能发生这个异常,理论上异步请求返回后执行commit时,此界面已经onSaveInstanceState或者onStop了,那就会抛出这个异常了。
三、异常解决方案
综合以上分析,现开始着手解决:
1、确保transaction执行commit时,界面处于onSaveInstanceState和onStop 前。
2、onSaveInstanceState 后,保存界面关键参数。
2、假如界面会被重新创建恢复状态,先获取关键参数,再去执行commit操作。
我们知道onSaveInstanceState()方法是在当 手机内存不足,回收activity之前调用,一般是在onPause()之后,肯定是在onStop之前,我们可以在此时做下记录,比如:
var mCurrentPosition:Int = 0
var mUserID:String?=null
var mCurrentFragmentType:Int = 0
var mAfterSavedState:Boolean = false
var mStopp = false
override fun onSaveInstanceState(outState: Bundle) {
outState.putString("user_id",mUserID)
outState.putInt("current_position",mCurrentPosition)
outState.putInt("current_fragmentType",mCurrentFragmentType)
super.onSaveInstanceState(outState)
mAfterSavedState = true
}
override fun onStop() {
super.onStop()
mStopState = true
}
然后在transaction执行commit时去做判断,在activity恢复重新创建时获取关键值,比如:
// 异步请求回来后,先记录数据值,如果不满足条件,不提交事务
if(mAfterSavedState||mStopState) return
// transaction 操作,如add,show
//commit
transaction.commit()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(savedInstanceState!=null){ //activity 被回收后从新创建
mUserID = savedInstanceState.getString("user_id")
mCurrentFragmentType = savedInstanceState.getInt("current_fragmentType")
mCurrentPosition= savedInstanceState.getInt("current_position")
}else{
mUserID = intent.getStringExtra("user_id")
}
// 请求网络,加载数据
// 请求网络,网络回来后还要判断数据,执行transaction
}
override fun onResume() {
super.onResume()
// 这里不要忘记重置标记
mAfterSavedState = false
mStopState = false
// 根据参数,执行stop前 未执行的transaction
//transaction.commit()
}
override fun onPause() {
super.onPause()
//mCurrentPosition =
}
当然了 你也可以使用onRestoreInstanceState方法,如果activity被回收重新创建,此方法肯定会走,Bundle直接取值即可,这里需要注意,网络请求不要放在onCreate方法了,因为请求需要的参数onCreate时拿不到,放到onRestoreInstanceState之后的onResume方法中处理,根据具体需求决定是否需要每次Resume都加载数据:
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
mUserID = savedInstanceState.getString("user_id")
mCurrentFragmentType = savedInstanceState.getInt("current_fragmentType")
mCurrentPosition= savedInstanceState.getInt("current_position")
}
override fun onResume() {
super.onResume()
// 这里不要忘记重置标记
mAfterSavedState = false
mStopState = false
if(mLoadData){
// 请求网络,网络回来 后还要判断数据,执行transaction
}
// 根据参数,执行stop前 未执行的transaction
//transaction.commit()
}
到这里此异常的解决方案基本总结完毕,可以稍微封装一下,把状态值mAfterSavedState和mStopState 放到基类BaseActivity中,基类中直接记录这俩状态,子类直接使用;父类封装commit方法供子类使用,commit之前检查状态;具体界面的onSaveInstanceState各自去处理,查看FragmentActivity中,有mCreated,mResumed,mStopped这些状态值,只不过没有暴露给子类,尬- -。