FragmentManager checkStateLoss

一、问题

先来看两个Crash Log:

1.

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
    at android.support.v4.app.ab.v(FragmentManager.java:1377)
    at android.support.v4.app.ab.a(FragmentManager.java:1395)
    at android.support.v4.app.h.a(BackStackRecord.java:637)
    at android.support.v4.app.h.b(BackStackRecord.java:616)
    at android.support.v4.app.DialogFragment.show(DialogFragment.java:139)
    at com.sankuai.common.views.ai.a(MaoyanDialogBuilder.java:184)
    at com.sankuai.movie.y.handleMessage(MovieMainActivity.java:750)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:137)
    at android.app.ActivityThread.main(ActivityThread.java:4424)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:511)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
    at dalvik.system.NativeStart.main(Native Method)

2.

java.lang.IllegalStateException: Can not perform this action inside of onLoadFinished
 at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1381)
 at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1395)
 at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:637)
 at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:616)
 at android.support.v4.app.DialogFragment.show(DialogFragment.java:139)
 at com.sankuai.common.views.MaoyanDialogBuilder.show(MaoyanDialogBuilder.java:185)
 at com.sankuai.movie.MovieMainActivity$8$1$1.onLoadFinished(MovieMainActivity.java:609)
 at com.sankuai.movie.MovieMainActivity$8$1$1.onLoadFinished(MovieMainActivity.java:590)
 at android.support.v4.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:427)
 at android.support.v4.app.LoaderManagerImpl$LoaderInfo.onLoadComplete(LoaderManager.java:395)
 at android.support.v4.content.Loader.deliverResult(Loader.java:104)

二、原因

经过查找,发现这两个Crash处于由同一个方法触发:

FragmentManagerImpl#checkStateLoss():
private void checkStateLoss() {
    if (mStateSaved) {
        throw new IllegalStateException(
                "Can not perform this action after onSaveInstanceState");
    }
    if (mNoTransactionsBecause != null) {
        throw new IllegalStateException(
                "Can not perform this action inside of " + mNoTransactionsBecause);
    }
}

这个方法中的每一个异常分别对应了上述的两段Crash Log。下面逐一分析。

三、分析

第一部分:mStateSaved:

关于这个问题,可以参考这篇文章,Fragment Transactions & Activity State Loss

从代码中可以看出,若这个字段为true,则会抛出异常。检查代码中与这个字段相关的内容,如下:

Parcelable saveAllState() {
    // Make sure all pending operations have now been executed to get
    // our state update-to-date.
    execPendingActions();
 
    if (HONEYCOMB) {
        // As of Honeycomb, we save state after pausing.  Prior to that
        // it is before pausing.  With fragments this is an issue, since
        // there are many things you may do after pausing but before
        // stopping that change the fragment state.  For those older
        // devices, we will not at this point say that we have saved
        // the state, so we will allow them to continue doing fragment
        // transactions.  This retains the same semantics as Honeycomb,
        // though you do have the risk of losing the very most recent state
        // if the process is killed...  we'll live with that.
        mStateSaved = true;
    }
    ...
}
  
public void noteStateNotSaved() {
    mStateSaved = false;
}
 
public void dispatchCreate() {
    mStateSaved = false;
    moveToState(Fragment.CREATED, false);
}
 
public void dispatchActivityCreated() {
    mStateSaved = false;
    moveToState(Fragment.ACTIVITY_CREATED, false);
}
 
public void dispatchStart() {
    mStateSaved = false;
    moveToState(Fragment.STARTED, false);
}
 
public void dispatchResume() {
    mStateSaved = false;
    moveToState(Fragment.RESUMED, false);
}
 
public void dispatchPause() {
    moveToState(Fragment.STARTED, false);
}
 
public void dispatchStop() {
    // See saveAllState() for the explanation of this.  We do this for
    // all platform versions, to keep our behavior more consistent between
    // them.
    mStateSaved = true;
 
    moveToState(Fragment.STOPPED, false);
}

可以看到,这个字段的使用与生命周期有关,随便找一个生命周期的传递方法去查看使用:

会有三处使用到,分别研究

1)Fragment#getChildFragmentManager

/**
 * Return a private FragmentManager for placing and managing Fragments
 * inside of this Fragment.
 */
final public FragmentManager getChildFragmentManager() {
    if (mChildFragmentManager == null) {
        instantiateChildFragmentManager();
        if (mState >= RESUMED) {
            mChildFragmentManager.dispatchResume();
        } else if (mState >= STARTED) {
            mChildFragmentManager.dispatchStart();
        } else if (mState >= ACTIVITY_CREATED) {
            mChildFragmentManager.dispatchActivityCreated();
        } else if (mState >= CREATED) {
            mChildFragmentManager.dispatchCreate();
        }
    }
    return mChildFragmentManager;
}

这是在Fragment中获取嵌套使用Fragment,获取childFragmentManager时调用,即告知childFragmentManager父Fragment当前的生命周期。此时也会执行childFragmentManager的初始化。

2)Fragment#performStart

void performStart() {
    if (mChildFragmentManager != null) {
        mChildFragmentManager.noteStateNotSaved();
        mChildFragmentManager.execPendingActions();
    }
    mCalled = false;
    onStart();
    if (!mCalled) {
        throw new SuperNotCalledException("Fragment " + this
                + " did not call through to super.onStart()");
    }
    if (mChildFragmentManager != null) {
        mChildFragmentManager.dispatchStart();
    }
    if (mLoaderManager != null) {
        mLoaderManager.doReportStart();
    }
}

这是在Fragmen在自己的生命周期变化过程中,通知子Fragment。

3)FragmentActivity#onStart

/**
 * Dispatch onStart() to all fragments.  Ensure any created loaders are
 * now started.
 */
@Override
protected void onStart() {
    super.onStart();
 
    mStopped = false;
    mReallyStopped = false;
    mHandler.removeMessages(MSG_REALLY_STOPPED);
 
    if (!mCreated) {
        mCreated = true;
        mFragments.dispatchActivityCreated();
    }
 
    mFragments.noteStateNotSaved();
    mFragments.execPendingActions();
     
    if (!mLoadersStarted) {
        mLoadersStarted = true;
        if (mLoaderManager != null) {
            mLoaderManager.doStart();
        } else if (!mCheckedForLoaderManager) {
            mLoaderManager = getLoaderManager("(root)", mLoadersStarted, false);
            // the returned loader manager may be a new one, so we have to start it
            if ((mLoaderManager != null) && (!mLoaderManager.mStarted)) {
                mLoaderManager.doStart();
            }
        }
        mCheckedForLoaderManager = true;
    }
    // NOTE: HC onStart goes here.
     
    mFragments.dispatchStart();
    if (mAllLoaderManagers != null) {
        final int N = mAllLoaderManagers.size();
        LoaderManagerImpl loaders[] = new LoaderManagerImpl[N];
        for (int i=N-1; i>=0; i--) {
            loaders[i] = mAllLoaderManagers.valueAt(i);
        }
        for (int i=0; i<N; i++) {
            LoaderManagerImpl lm = loaders[i];
            lm.finishRetain();
            lm.doReportStart();
        }
    }
}

这是Activity在生命周期发生变化时,通知Fragment。

现在了解了这个行为是和整个Activity、Fragment的生命周期有关的,回过头再来看mStateSaved,这个字段是在onPause或者onStop之后就被置为true。在Android中,由于对运行时的生命周期应用能做的实在是很少,用户可以随时切换Activity,系统也可以随时回收处于后台的Activity内存,所以Android为了保证在再次返回Activity时让它看起来同离开时相似,会使用onSaveInstanceState()来保存一些状态。若在onSaveInstanceState()被调用之后调用FragmentTransaction#commit(),那么这个Fragment的状态就不会被保存,在之后恢复时也不会恢复这个Fragment,使得恢复时UI发生一些变化。
那为什么是onPause或者onStop之后被置为true?这和Android的版本发展有关,在HoneyComb之前,Activity被设计成在onPause之前不会被杀掉,所以onSaveInstanceState()是紧挨着onPause()之前调用的,但是在HoneyComb之后,Activity被设计成只有在onStop()之后才会被杀死,所以onSaveInstanceState()会在onStop()之前,而不是在onPause之前调用。

pre-Honeycomb
post-Honeycomb
Activities can be killed before onPause()?  NO  NO
Activities can be killed before onStop()?   YES NO
onSaveInstanceState(Bundle) is guaranteed to be called before...    onPause()   onStop()

由于这个原因,若是在旧机型的onPause()之后调用FragmentTransaction#commit(),这个状态就可能会丢失。这是由于Android开发者为了避免过多的异常而做出的让步,允许在onPause()和onStop()之间偶尔丢失commit()状态。

pre-Honeycomb
post-Honeycomb
commit() before onPause()   OK  OK
commit() between onPause() and onStop() STATE LOSS  OK
commit() after onStop() EXCEPTION   
EXCEPTION

应当注意的是,不仅仅是Activity的生命周期会影响,若使用的是嵌套在Fragment中的子Fragment,由上述代码可知,也会有类似情况。

第二部分:mNoTransactionsBecause

再看一下FragmentManagerImpl#checkStateLoss()方法,

private void checkStateLoss() {
    if (mStateSaved) {
        throw new IllegalStateException(
                "Can not perform this action after onSaveInstanceState");
    }
    if (mNoTransactionsBecause != null) {
        throw new IllegalStateException(
                "Can not perform this action inside of " + mNoTransactionsBecause);
    }
}

还有一个和StateLoss相关的字段是mNoTransactionsBecause,若这个字段不为空,则抛出异常。先查看这个字段在哪里使用,或者说,在什么情况下这个字段会不为空。

可以看到调用的地方主要是两个类,FragmentManager 和 LoaderManager,在FragmentManager中的使用都是用于抛出异常的,并不在意这个,而是在意何时不为空的,在LoaderManager中主要两个地方为这个字段赋值:

1)destory

void destroy() {
    if (DEBUG) Log.v(TAG, "  Destroying: " + this);
    mDestroyed = true;
    boolean needReset = mDeliveredData;
    mDeliveredData = false;
    if (mCallbacks != null && mLoader != null && mHaveData && needReset) {
        if (DEBUG) Log.v(TAG, "  Reseting: " + this);
        String lastBecause = null;
        if (mActivity != null) {
            lastBecause = mActivity.mFragments.mNoTransactionsBecause;
            mActivity.mFragments.mNoTransactionsBecause = "onLoaderReset";
        }
        try {
            mCallbacks.onLoaderReset(mLoader);
        } finally {
            if (mActivity != null) {
                mActivity.mFragments.mNoTransactionsBecause = lastBecause;
            }
        }
    }
    mCallbacks = null;
    mData = null;
    mHaveData = false;
    if (mLoader != null) {
        if (mListenerRegistered) {
            mListenerRegistered = false;
            mLoader.unregisterListener(this);
        }
        mLoader.reset();
    }
    if (mPendingLoader != null) {
        mPendingLoader.destroy();
    }
}

2)callOnLoadFinished

void callOnLoadFinished(Loader<Object> loader, Object data) {
    if (mCallbacks != null) {
        String lastBecause = null;
        if (mActivity != null) {
            lastBecause = mActivity.mFragments.mNoTransactionsBecause;
            mActivity.mFragments.mNoTransactionsBecause = "onLoadFinished";
        }
        try {
            if (DEBUG) Log.v(TAG, "  onLoadFinished in " + loader + ": "
                    + loader.dataToString(data));
            mCallbacks.onLoadFinished(loader, data);
        } finally {
            if (mActivity != null) {
                mActivity.mFragments.mNoTransactionsBecause = lastBecause;
            }
        }
        mDeliveredData = true;
    }

在这两个地方的考量,也是由于loader的异步可能导致fragment在onSaveInstanceState()之后调用导致状态丢失。

四、解决方案

那么如何解决这个问题?

1)在LoaderManager.LoaderCallbacks#onLoadFinished 或者 LoaderManager.LoaderCallbacks#onLoaderReset中使用

这里直接使用commit()方法会直接抛出异常,需要加Handler避免在这两个回调函数中直接使用commit()方法

2)在使用FragmentTransaction#commit()方法时注意当前的生命周期。

一般而言,会在onCreate()或者响应用户的操作事件时才会使用commit()方法,这不会有问题,但假若在别的生命周期中使用就要小心了。例如onActivityResult(), onStart(), 和 onResume(),就需要注意。例如,在onResume()中使用,但onResume()并不会保证在Activity状态恢复之后调用,此时需要使用FragmentActivity#onResumeFragments()或者Activity#onPostResume()中调用,这两个会保证在状态恢复之后调用。

3)避免异步回调中使用commit()方法

异步回调,如AsyncTask#onPostExecute() 和 LoaderManager.LoaderCallbacks#onLoadFinished(),之后无法保证当前的生命状态,而且异步通常会执行一些比较耗时的操作,更容易使得这样的丢失发生,如用户发出一个请求之后点了HOME键。

4)使用 commitAllowingStateLoss()方法

这个方法会跳过mStateSaved的检查,也不会在意会发生怎样的影响,就算无法执行或Activity状态恢复之后发生了UI变动也不会有警报。

5)针对DialogFragment的解决方案

由于DialogFragment和其它Fragment相比比较特殊,创建、回收更频繁也更不容易控制。

方法一:

对于DialogFragment而言,只有show()方法而没有showAllowingStateLoss()方法。。。而且很多时候都需要在网络请求返回之后根据返回的字段来显示,所以最好在base中添加对Activity和Fragment的生命周期追踪方法。但自己添加的方法和原生的毕竟在执行时间上还是有一点时间差的,不能够100%避免crash。

方法二:

或许有想法说可以重写DialogFragment#show()方法,让它支持commitAllowingStateLoss(),好吧,来看下源码。。。这是show方法:

/**
 * Display the dialog, adding the fragment to the given FragmentManager.  This
 * is a convenience for explicitly creating a transaction, adding the
 * fragment to it with the given tag, and committing it.  This does
 * <em>not</em> add the transaction to the back stack.  When the fragment
 * is dismissed, a new transaction will be executed to remove it from
 * the activity.
 * @param manager The FragmentManager this fragment will be added to.
 * @param tag The tag for this fragment, as per
 * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
 */
public void show(FragmentManager manager, String tag) {
    mDismissed = false;
    mShownByMe = true;
    FragmentTransaction ft = manager.beginTransaction();
    ft.add(this, tag);
    ft.commit();
}
 
/**
 * Display the dialog, adding the fragment using an existing transaction
 * and then committing the transaction.
 * @param transaction An existing transaction in which to add the fragment.
 * @param tag The tag for this fragment, as per
 * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.
 * @return Returns the identifier of the committed transaction, as per
 * {@link FragmentTransaction#commit() FragmentTransaction.commit()}.
 */
public int show(FragmentTransaction transaction, String tag) {
    mDismissed = false;
    mShownByMe = true;
    transaction.add(this, tag);
    mViewDestroyed = false;
    mBackStackId = transaction.commit();
    return mBackStackId;
}

可以看到,和普通的Fragment显示方法区别并不大,其实DialogFragment只是一个Fragment里面套了个Dialog而已。但,有个很神奇的字段,mShownByMe,这个字段是做什么的?定义处和这里并没有注释,通过查找这个字段的使用,发现了这两个方法:

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    if (!mShownByMe) {
        // If not explicitly shown through our API, take this as an
        // indication that the dialog is no longer dismissed.
        mDismissed = false;
    }
}
 
@Override
public void onDetach() {
    super.onDetach();
    if (!mShownByMe && !mDismissed) {
        // The fragment was not shown by a direct call here, it is not
        // dismissed, and now it is being detached...  well, okay, thou
        // art now dismissed.  Have fun.
        mDismissed = true;
    }
}

通过这里可以发现,这个字段是判断DialogFragment是不是通过原生API的show()方法来显示的。。否则就不在onAttach()和onDetach()里设置mDismissed字段的,再看onDetach里的注释,为什么有种深深恶意。。(我辛辛苦苦写好了API,你为什么不用?你为什么不用?你为什么不用?(╯‵□′)╯︵┻━┻)
重写show()也是可以的,但需要紧接着重写onAttach()、onDetach()等方法。
若有需要重写show(FragmentTransaction transaction, String tag)方法,除了mShownByMe字段还需要注意mViewDestroyed字段的值设置。

其实对于重写show()方法的弊端主要在于无法保证show()的执行,从而导致isShowing()等方法的判断不准确,引起一些其他的问题。

方法三:

弃用DialogFragment。这种方法可以避免如此繁复的生命周期,代码和使用简洁不少,但也少了 DialogFragment 的优势,例如不能在切换屏幕时保留 Dialog 等。

方法一的优势在于并不需要对DialogFragment的内部实现做详尽的了解,也避免了开发者的恶意,但在onPause()之后,onSaveInstanceState()之前的dialog都不会展示出来。

方法二的优势在于API使用者并不需要在调用API的同时还为它的上下文环境担惊受怕,但重写难度较大,而且会有状态丢失的情况。

方法三也不是不可,只是有些问题只适用 DialogFragment 解决,手动实现会有很多变数。

自行斟酌使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,873评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,483评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,525评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,595评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,018评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,958评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,118评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,873评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,643评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,813评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,293评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,615评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,306评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,170评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,968评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,107评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,894评论 2 278

推荐阅读更多精彩内容