FragmentTransaction commit Can not perform this action after onSaveInstanceState

http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html 


[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState  
  2.     at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1341)  
  3.     at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1352)  
  4.     at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)  
  5.     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()是理解关键。

首先附上的是存储相关的源码:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /** 
  2.  * The hook for {@link ActivityThread} to save the state of this activity. 
  3.  * 
  4.  * Calls {@link #onSaveInstanceState(android.os.Bundle)} 
  5.  * and {@link #saveManagedDialogs(android.os.Bundle)}. 
  6.  * 
  7.  * @param outState The bundle to save the state to. 
  8.  */  
  9. final void performSaveInstanceState(Bundle outState) {  
  10.     onSaveInstanceState(outState);  
  11.     saveManagedDialogs(outState);  
  12. }  
  13.   
  14. /** 
  15.  * Called to retrieve per-instance state from an activity before being killed 
  16.  * so that the state can be restored in {@link #onCreate} or 
  17.  * {@link #onRestoreInstanceState} (the {@link Bundle} populated by this method 
  18.  * will be passed to both). 
  19.  * 
  20.  * <p>This method is called before an activity may be killed so that when it 
  21.  * comes back some time in the future it can restore its state.  For example, 
  22.  * if activity B is launched in front of activity A, and at some point activity 
  23.  * A is killed to reclaim resources, activity A will have a chance to save the 
  24.  * current state of its user interface via this method so that when the user 
  25.  * returns to activity A, the state of the user interface can be restored 
  26.  * via {@link #onCreate} or {@link #onRestoreInstanceState}. 
  27.  * 
  28.  * <p>Do not confuse this method with activity lifecycle callbacks such as 
  29.  * {@link #onPause}, which is always called when an activity is being placed 
  30.  * in the background or on its way to destruction, or {@link #onStop} which 
  31.  * is called before destruction.  One example of when {@link #onPause} and 
  32.  * {@link #onStop} is called and not this method is when a user navigates back 
  33.  * from activity B to activity A: there is no need to call {@link #onSaveInstanceState} 
  34.  * on B because that particular instance will never be restored, so the 
  35.  * system avoids calling it.  An example when {@link #onPause} is called and 
  36.  * not {@link #onSaveInstanceState} is when activity B is launched in front of activity A: 
  37.  * the system may avoid calling {@link #onSaveInstanceState} on activity A if it isn't 
  38.  * killed during the lifetime of B since the state of the user interface of 
  39.  * A will stay intact. 
  40.  * 
  41.  * <p>The default implementation takes care of most of the UI per-instance 
  42.  * state for you by calling {@link android.view.View#onSaveInstanceState()} on each 
  43.  * view in the hierarchy that has an id, and by saving the id of the currently 
  44.  * focused view (all of which is restored by the default implementation of 
  45.  * {@link #onRestoreInstanceState}).  If you override this method to save additional 
  46.  * information not captured by each individual view, you will likely want to 
  47.  * call through to the default implementation, otherwise be prepared to save 
  48.  * all of the state of each view yourself. 
  49.  * 
  50.  * <p>If called, this method will occur before {@link #onStop}.  There are 
  51.  * no guarantees about whether it will occur before or after {@link #onPause}. 
  52.  *  
  53.  * @param outState Bundle in which to place your saved state. 
  54.  *  
  55.  * @see #onCreate 
  56.  * @see #onRestoreInstanceState 
  57.  * @see #onPause 
  58.  */  
  59. protected void onSaveInstanceState(Bundle outState) {  
  60.     outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());  
  61.     Parcelable p = mFragments.saveAllState();  
  62.     if (p != null) {  
  63.         outState.putParcelable(FRAGMENTS_TAG, p);  
  64.     }  
  65.     getApplication().dispatchActivitySaveInstanceState(this, outState);  
  66. }  
  67.   
  68. /** 
  69.  * Save the state of any managed dialogs. 
  70.  * 
  71.  * @param outState place to store the saved state. 
  72.  */  
  73. private void saveManagedDialogs(Bundle outState) {  
  74.     if (mManagedDialogs == null) {  
  75.         return;  
  76.     }  
  77.   
  78.     final int numDialogs = mManagedDialogs.size();  
  79.     if (numDialogs == 0) {  
  80.         return;  
  81.     }  
  82.   
  83.     Bundle dialogState = new Bundle();  
  84.   
  85.     int[] ids = new int[mManagedDialogs.size()];  
  86.   
  87.     // save each dialog's bundle, gather the ids  
  88.     for (int i = 0; i < numDialogs; i++) {  
  89.         final int key = mManagedDialogs.keyAt(i);  
  90.         ids[i] = key;  
  91.         final ManagedDialog md = mManagedDialogs.valueAt(i);  
  92.         dialogState.putBundle(savedDialogKeyFor(key), md.mDialog.onSaveInstanceState());  
  93.         if (md.mArgs != null) {  
  94.             dialogState.putBundle(savedDialogArgsKeyFor(key), md.mArgs);  
  95.         }  
  96.     }  
  97.   
  98.     dialogState.putIntArray(SAVED_DIALOG_IDS_KEY, ids);  
  99.     outState.putBundle(SAVED_DIALOGS_TAG, dialogState);  
  100. }  
接着附上还原相关的源码:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /** 
  2.  * Called when the activity is starting.  This is where most initialization 
  3.  * should go: calling {@link #setContentView(int)} to inflate the 
  4.  * activity's UI, using {@link #findViewById} to programmatically interact 
  5.  * with widgets in the UI, calling 
  6.  * {@link #managedQuery(android.net.Uri , String[], String, String[], String)} to retrieve 
  7.  * cursors for data being displayed, etc. 
  8.  *  
  9.  * <p>You can call {@link #finish} from within this function, in 
  10.  * which case onDestroy() will be immediately called without any of the rest 
  11.  * of the activity lifecycle ({@link #onStart}, {@link #onResume}, 
  12.  * {@link #onPause}, etc) executing. 
  13.  *  
  14.  * <p><em>Derived classes must call through to the super class's 
  15.  * implementation of this method.  If they do not, an exception will be 
  16.  * thrown.</em></p> 
  17.  *  
  18.  * @param savedInstanceState If the activity is being re-initialized after 
  19.  *     previously being shut down then this Bundle contains the data it most 
  20.  *     recently supplied in {@link #onSaveInstanceState}.  <b><i>Note: Otherwise it is null.</i></b> 
  21.  *  
  22.  * @see #onStart 
  23.  * @see #onSaveInstanceState 
  24.  * @see #onRestoreInstanceState 
  25.  * @see #onPostCreate 
  26.  */  
  27. protected void onCreate(Bundle savedInstanceState) {  
  28.     if (mLastNonConfigurationInstances != null) {  
  29.         mAllLoaderManagers = mLastNonConfigurationInstances.loaders;  
  30.     }  
  31.     if (savedInstanceState != null) {  
  32.         Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);  
  33.         mFragments.restoreAllState(p, mLastNonConfigurationInstances != null  
  34.                 ? mLastNonConfigurationInstances.fragments : null);  
  35.     }  
  36.     mFragments.dispatchCreate();  
  37.     getApplication().dispatchActivityCreated(this, savedInstanceState);  
  38.     mCalled = true;  
  39. }  
  40.   
  41. /** 
  42.  * The hook for {@link ActivityThread} to restore the state of this activity. 
  43.  * 
  44.  * Calls {@link #onSaveInstanceState(android.os.Bundle)} and 
  45.  * {@link #restoreManagedDialogs(android.os.Bundle)}. 
  46.  * 
  47.  * @param savedInstanceState contains the saved state 
  48.  */  
  49. final void performRestoreInstanceState(Bundle savedInstanceState) {  
  50.     onRestoreInstanceState(savedInstanceState);  
  51.     restoreManagedDialogs(savedInstanceState);  
  52. }  
  53.   
  54. /** 
  55.  * This method is called after {@link #onStart} when the activity is 
  56.  * being re-initialized from a previously saved state, given here in 
  57.  * <var>savedInstanceState</var>.  Most implementations will simply use {@link #onCreate} 
  58.  * to restore their state, but it is sometimes convenient to do it here 
  59.  * after all of the initialization has been done or to allow subclasses to 
  60.  * decide whether to use your default implementation.  The default 
  61.  * implementation of this method performs a restore of any view state that 
  62.  * had previously been frozen by {@link #onSaveInstanceState}. 
  63.  *  
  64.  * <p>This method is called between {@link #onStart} and 
  65.  * {@link #onPostCreate}. 
  66.  *  
  67.  * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}. 
  68.  *  
  69.  * @see #onCreate 
  70.  * @see #onPostCreate 
  71.  * @see #onResume 
  72.  * @see #onSaveInstanceState 
  73.  */  
  74. protected void onRestoreInstanceState(Bundle savedInstanceState) {  
  75.     if (mWindow != null) {  
  76.         Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);  
  77.         if (windowState != null) {  
  78.             mWindow.restoreHierarchyState(windowState);  
  79.         }  
  80.     }  
  81. }  
  82.   
  83. /** 
  84.  * Restore the state of any saved managed dialogs. 
  85.  * 
  86.  * @param savedInstanceState The bundle to restore from. 
  87.  */  
  88. private void restoreManagedDialogs(Bundle savedInstanceState) {  
  89.     final Bundle b = savedInstanceState.getBundle(SAVED_DIALOGS_TAG);  
  90.     if (b == null) {  
  91.         return;  
  92.     }  
  93.   
  94.     final int[] ids = b.getIntArray(SAVED_DIALOG_IDS_KEY);  
  95.     final int numDialogs = ids.length;  
  96.     mManagedDialogs = new SparseArray<ManagedDialog>(numDialogs);  
  97.     for (int i = 0; i < numDialogs; i++) {  
  98.         final Integer dialogId = ids[i];  
  99.         Bundle dialogState = b.getBundle(savedDialogKeyFor(dialogId));  
  100.         if (dialogState != null) {  
  101.             // Calling onRestoreInstanceState() below will invoke dispatchOnCreate  
  102.             // so tell createDialog() not to do it, otherwise we get an exception  
  103.             final ManagedDialog md = new ManagedDialog();  
  104.             md.mArgs = b.getBundle(savedDialogArgsKeyFor(dialogId));  
  105.             md.mDialog = createDialog(dialogId, dialogState, md.mArgs);  
  106.             if (md.mDialog != null) {  
  107.                 mManagedDialogs.put(dialogId, md);  
  108.                 onPrepareDialog(dialogId, md.mDialog, md.mArgs);  
  109.                 md.mDialog.onRestoreInstanceState(dialogState);  
  110.             }  
  111.         }  
  112.     }  
  113. }  

OK,下面接着回到那个问题——为什么异常会被抛出呢?事实上,这个问题的源头就是在某个时候Activity的onSaveInstanceState()方法被调用了,然后我们在那个时间点之后调用FragmentTransaction#commit()。于是这个Fragment事务所执行后的操作将不会被记录(这个作为Activity一部分的Fragment不被记录),因此对用户来说这个Fragment事务就丢失了,从而导致UI状态也丢失了。为了保证用户体验,Android就只是简单的抛出一个IllegalStateException,避免状态丢失造成的不良影响。

异常抛出时间点

  1. 在Honeycomb之前(不包含Android3.0),Activity只有到了onPause()生命周期后才能被系统杀死。这就意味着onSaveInstanceState()方法是在onPause()之前被调用了。
  2. 在Honeycomb之后(包含Android3.0),Activity只有到了onStop()生命周期后才能被系统杀死。这就意味着onSaveInstanceState()方法是在onStop()之前被调用了。

Activity具体能在哪些生命周期被系统杀死,可见下表:
生命周期方法killable?(当前生命周期,Activity能否被系统杀死)
onCreateno
onRestart()no
onStart()no
onResume()no
onPause()yes(sdkVersion<3.0),no(sdkVersion>=3.0)
onStop()yes
onDestroy()yes


Honeycomb之后(包含Android3.0)的系统上,只要在onSaveInstanceState()后调用FragmentTransaction#commit(),每次都会抛出一个异常,警告开发者state loss已经出现了。Honeycomb系统前后,Activity被系统杀死的时间点有轻微差异。于是,Android支持库根据不同版本号提供不同的提示。比如:
  • Honeycomb之前(不包含Android3.0)的系统上,由于onSaveInstanceState()的调用点可能会比3.0后的系统更早(出现在onPause()),于是Android做一个区分:在onPause()与onStop()之间调用FragmentTransaction#commit(),抛出state loss;在onStop()之后调用,直接抛出异常
支持库在不同版本上的差异行为如下表:

FragmentTransaction#commit()调用点Honeycomb之前(3.0以下,不包含3.0)Honeycomb之后(3.0以上,包含3.0)
onPause()之前前OKOK
onPause()与onStop()之间State LossOK
onStop()之后ExceptionException

总结

Fragment事物执行必须要在onSaveInstanceState()回调方法之前调用,否则会抛出异常,导致程序Crash。

Additional:

  1. Fragment事物执行必须要在UI线程中执行,否则程序会Crash
  2. Fragment事物执行也是有一定的耗时的,如果需要提交执行的Fragment事物太多了的话,会造成延迟几秒,更糟糕的情况甚至是ANR。(想象事物要执行add,delete的Fragment有10个、100个或者更多)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值