java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1341)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1352)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:574)
上面这段异常栈信息,凡是操作过Fragmen事务t的人都应该熟悉吧。这段异常栈简单来说,就是在onSaveInstanceState()方法被调用后执行FragmentTransaction#commit(),由于状态丢失,而抛出IllegalStateException。
很遗憾的是,关于这个异常,在Android SDK文档中基本没有提及,只有在Transactions最后的Caution提到一句话——只能在Activity保存状态(用户离开Activity)前,使用commit()方法提交Fragment事务。
由于Android权限机制的原因,Android应用程序对于Android运行环境没有多少控制能力。然而Android系统有能力为了释放内存杀死应用进程,或者在没有任何警告的情况下杀死一个Background Activity。为了确保对用户隐藏这些偶发不稳定的行为,Android框架为每一个Activity都提供了一个Activity是否被onSaveInstanceState()方法,用来在Activity被破坏时存储它的状态。当被保存的状态还原时,这会给用户一个无缝的体验,用户返回Background Activity(不管Android系统是否将Background Activity杀死重建),看起来就像是Foregroun Activity和Background Activity之间的切换。
Additional:当Android框架调用onSaveInstanceState()时,Android框架会通过该方法传递一个Bundle对象,供Activity保存状态(Activity默认会记录View,Dialog和Fragment的状态,当然可以重写这个方法,记录一些其它的数据)。当onSaveInstanceState()返回时,就意味着系统将打包完成的Bundle对象通过Binder接口到达了系统服务进程(一个安全的存储地方)。当系统决定重建之前被杀死的Activity时,系统就会将之前存储的Bundle对象返回给应用,使其能够恢复Activity的原先状态。
为了证明上述Additional的真实性,下面附上两段Activity中的源码,分别是关于存储和还原的,相信只要仔细看下这两段代码就能理解意思了。performSaveInstanceState(),performRestoreInstanceState和onCreate()是理解关键。
首先附上的是存储相关的源码:
/**
* The hook for {@link ActivityThread} to save the state of this activity.
*
* Calls {@link #onSaveInstanceState(android.os.Bundle)}
* and {@link #saveManagedDialogs(android.os.Bundle)}.
*
* @param outState The bundle to save the state to.
*/
final void performSaveInstanceState(Bundle outState) {
onSaveInstanceState(outState);
saveManagedDialogs(outState);
}
/**
* Called to retrieve per-instance state from an activity before being killed
* so that the state can be restored in {@link #onCreate} or
* {@link #onRestoreInstanceState} (the {@link Bundle} populated by this method
* will be passed to both).
*
* This method is called before an activity may be killed so that when it
* comes back some time in the future it can restore its state. For example,
* if activity B is launched in front of activity A, and at some point activity
* A is killed to reclaim resources, activity A will have a chance to save the
* current state of its user interface via this method so that when the user
* returns to activity A, the state of the user interface can be restored
* via {@link #onCreate} or {@link #onRestoreInstanceState}.
*
*
Do not confuse this method with activity lifecycle callbacks such as
* {@link #onPause}, which is always called when an activity is being placed
* in the background or on its way to destruction, or {@link #onStop} which
* is called before destruction. One example of when {@link #onPause} and
* {@link #onStop} is called and not this method is when a user navigates back
* from activity B to activity A: there is no need to call {@link #onSaveInstanceState}
* on B because that particular instance will never be restored, so the
* system avoids calling it. An example when {@link #onPause} is called and
* not {@link #onSaveInstanceState} is when activity B is launched in front of activity A:
* the system may avoid calling {@link #onSaveInstanceState} on activity A if it isn't
* killed during the lifetime of B since the state of the user interface of
* A will stay intact.
*
*
The default implementation takes care of most of the UI per-instance
* state for you by calling {@link android.view.View#onSaveInstanceState()} on each
* view in the hierarchy that has an id, and by saving the id of the currently
* focused view (all of which is restored by the default implementation of
* {@link #onRestoreInstanceState}). If you override this method to save additional
* information not captured by each individual view, you will likely want to
* call through to the default implementation, otherwise be prepared to save
* all of the state of each view yourself.
*
*
If called, this method will occur before {@link #onStop}. There are
* no guarantees about whether it will occur before or after {@link #onPause}.
*
* @param outState Bundle in which to place your saved state.
*
* @see #onCreate
* @see #onRestoreInstanceState
* @see #onPause
*/
protected void onSaveInstanceState(Bundle outState) {
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
getApplication().dispatchActivitySaveInstanceState(this, outState);
}
/**
* Save the state of any managed dialogs.
*
* @param outState place to store the saved state.
*/
private void saveManagedDialogs(Bundle outState) {
if (mManagedDialogs == null) {
return;
}
final int numDialogs = mManagedDialogs.size();
if (numDialogs == 0) {
return;
}
Bundle dialogState = new Bundle();
int[] ids = new int[mManagedDialogs.size()];
// save each dialog's bundle, gather the ids
for (int i = 0; i < numDialogs; i++) {
final int key = mManagedDialogs.keyAt(i);
ids[i] = key;
final ManagedDialog md = mManagedDialogs.valueAt(i);
dialogState.putBundle(savedDialogKeyFor(key), md.mDialog.onSaveInstanceState());
if (md.mArgs != null) {
dialogState.putBundle(savedDialogArgsKeyFor(key), md.mArgs);
}
}
dialogState.putIntArray(SAVED_DIALOG_IDS_KEY, ids);
outState.putBundle(SAVED_DIALOGS_TAG, dialogState);
}
接着附上还原相关的源码:
/**
* Called when the activity is starting. This is where most initialization
* should go: calling {@link #setContentView(int)} to inflate the
* activity's UI, using {@link #findViewById} to programmatically interact
* with widgets in the UI, calling
* {@link #managedQuery(android.net.Uri , String[], String, String[], String)} to retrieve
* cursors for data being displayed, etc.
*
* You can call {@link #finish} from within this function, in
* which case onDestroy() will be immediately called without any of the rest
* of the activity lifecycle ({@link #onStart}, {@link #onResume},
* {@link #onPause}, etc) executing.
*
*
Derived classes must call through to the super class's
* implementation of this method. If they do not, an exception will be
* thrown.
*
* @param savedInstanceState If the activity is being re-initialized after
* previously being shut down then this Bundle contains the data it most
* recently supplied in {@link #onSaveInstanceState}. Note: Otherwise it is null.
*
* @see #onStart
* @see #onSaveInstanceState
* @see #onRestoreInstanceState
* @see #onPostCreate
*/
protected void onCreate(Bundle savedInstanceState) {
if (mLastNonConfigurationInstances != null) {
mAllLoaderManagers = mLastNonConfigurationInstances.loaders;
}
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.fragments : null);
}
mFragments.dispatchCreate();
getApplication().dispatchActivityCreated(this, savedInstanceState);
mCalled = true;
}
/**
* The hook for {@link ActivityThread} to restore the state of this activity.
*
* Calls {@link #onSaveInstanceState(android.os.Bundle)} and
* {@link #restoreManagedDialogs(android.os.Bundle)}.
*
* @param savedInstanceState contains the saved state
*/
final void performRestoreInstanceState(Bundle savedInstanceState) {
onRestoreInstanceState(savedInstanceState);
restoreManagedDialogs(savedInstanceState);
}
/**
* This method is called after {@link #onStart} when the activity is
* being re-initialized from a previously saved state, given here in
* savedInstanceState. Most implementations will simply use {@link #onCreate}
* to restore their state, but it is sometimes convenient to do it here
* after all of the initialization has been done or to allow subclasses to
* decide whether to use your default implementation. The default
* implementation of this method performs a restore of any view state that
* had previously been frozen by {@link #onSaveInstanceState}.
*
* This method is called between {@link #onStart} and
* {@link #onPostCreate}.
*
* @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}.
*
* @see #onCreate
* @see #onPostCreate
* @see #onResume
* @see #onSaveInstanceState
*/
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
mWindow.restoreHierarchyState(windowState);
}
}
}
/**
* Restore the state of any saved managed dialogs.
*
* @param savedInstanceState The bundle to restore from.
*/
private void restoreManagedDialogs(Bundle savedInstanceState) {
final Bundle b = savedInstanceState.getBundle(SAVED_DIALOGS_TAG);
if (b == null) {
return;
}
final int[] ids = b.getIntArray(SAVED_DIALOG_IDS_KEY);
final int numDialogs = ids.length;
mManagedDialogs = new SparseArray(numDialogs);
for (int i = 0; i < numDialogs; i++) {
final Integer dialogId = ids[i];
Bundle dialogState = b.getBundle(savedDialogKeyFor(dialogId));
if (dialogState != null) {
// Calling onRestoreInstanceState() below will invoke dispatchOnCreate
// so tell createDialog() not to do it, otherwise we get an exception
final ManagedDialog md = new ManagedDialog();
md.mArgs = b.getBundle(savedDialogArgsKeyFor(dialogId));
md.mDialog = createDialog(dialogId, dialogState, md.mArgs);
if (md.mDialog != null) {
mManagedDialogs.put(dialogId, md);
onPrepareDialog(dialogId, md.mDialog, md.mArgs);
md.mDialog.onRestoreInstanceState(dialogState);
}
}
}
}
OK,下面接着回到那个问题——为什么异常会被抛出呢?事实上,这个问题的源头就是在某个时候Activity的onSaveInstanceState()方法被调用了,然后我们在那个时间点之后调用FragmentTransaction#commit()。于是这个Fragment事务所执行后的操作将不会被记录(这个作为Activity一部分的Fragment不被记录),因此对用户来说这个Fragment事务就丢失了,从而导致UI状态也丢失了。为了保证用户体验,Android就只是简单的抛出一个IllegalStateException,避免状态丢失造成的不良影响。
生命周期方法 | killable?(当前生命周期,Activity能否被系统杀死) |
onCreate | no |
onRestart() | no |
onStart() | no |
onResume() | no |
onPause() | yes(sdkVersion<3.0),no(sdkVersion>=3.0) |
onStop() | yes |
onDestroy() | yes |
FragmentTransaction#commit()调用点 | Honeycomb之前(3.0以下,不包含3.0) | Honeycomb之后(3.0以上,包含3.0) |
onPause()之前前 | OK | OK |
onPause()与onStop()之间 | State Loss | OK |
onStop()之后 | Exception | Exception |
Additional:
参考http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html