探索FragmentTransaction#commit()抛出IllegalStateException

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,避免状态丢失造成的不良影响。

异常抛出时间点

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

Activity具体能在哪些生命周期被系统杀死,可见下表:
生命周期方法 killable?(当前生命周期,Activity能否被系统杀死)
onCreate no
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()之前前 OK OK
onPause()与onStop()之间 State Loss OK
onStop()之后 Exception Exception

总结

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

Additional:

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



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

你可能感兴趣的:(Android经验总结)