探索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).
     *
     * <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

你可能感兴趣的:(android)