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).     *     * <p>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}.     *     * <p>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.     *     * <p>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.     *     * <p>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.     *      * <p>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.     *      * <p><em>Derived classes must call through to the super class's     * implementation of this method.  If they do not, an exception will be     * thrown.</em></p>     *      * @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}.  <b><i>Note: Otherwise it is null.</i></b>     *      * @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     * <var>savedInstanceState</var>.  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}.     *      * <p>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<ManagedDialog>(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

更多相关文章

  1. Android学习笔记-Android初级 (二)
  2. 解决:/system/bin/sh: ./hello: No such file or directory
  3. Android(安卓)Retrofit2网路编程实现方法详解
  4. Android存储登陆信息
  5. Android移动应用知识点总汇①
  6. Activity的基本理解
  7. Android(安卓)StudioRecyclerView,fragment,adapter的用法
  8. Android中模拟HOME键功能
  9. Android关于在Canvas类里的绘制线程问题汇总

随机推荐

  1. 我对Android(安卓)Activity的生命周期是
  2. Android中屏幕相关的操作
  3. Android 增加中文字体
  4. 数据存储有几种方式?分别是什么?
  5. 使用NDK的Cmake编译报错:Invalid Android
  6. Android之EditText 属性汇总
  7. android 相对布局中的 控件布局
  8. android 中的几种目录
  9. Android 4.0 对通知栏图标的尺寸有要求
  10. Android中webview跟JAVASCRIPT中的交互