非法状态异常:在ViewPager的onSaveInstanceState之后无法执行此操作。

570

我从应用市场获取用户报告,显示以下异常:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1109)
at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:399)
at android.app.Activity.onBackPressed(Activity.java:2066)
at android.app.Activity.onKeyUp(Activity.java:2044)
at android.view.KeyEvent.dispatch(KeyEvent.java:2529)
at android.app.Activity.dispatchKeyEvent(Activity.java:2274)
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1803)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchKeyEvent(PhoneWindow.java:1855)
at com.android.internal.policy.impl.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1277)
at android.app.Activity.dispatchKeyEvent(Activity.java:2269)
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1803)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.widget.TabHost.dispatchKeyEvent(TabHost.java:297)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112)
at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchKeyEvent(PhoneWindow.java:1855)
at com.android.internal.policy.impl.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1277)
at android.app.Activity.dispatchKeyEvent(Activity.java:2269)
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1803)
at android.view.ViewRoot.deliverKeyEventPostIme(ViewRoot.java:2880)
at android.view.ViewRoot.handleFinishedEvent(ViewRoot.java:2853)
at android.view.ViewRoot.handleMessage(ViewRoot.java:2028)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:132)
at android.app.ActivityThread.main(ActivityThread.java:4028)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:491)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:844)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:602)
at dalvik.system.NativeStart.main(Native Method)

显然这与FragmentManager有关,而我并没有使用它。堆栈跟踪中没有显示我的任何类,因此我不知道发生了什么异常以及如何预防它。

记录一下:我有一个选项卡主机,每个选项卡中都有一个ActivityGroup,在活动之间进行切换。


2
我发现了一个讨论相同问题的问题,但那里也没有解决方案。https://dev59.com/qms05IYBdhLWcg3wD90B - nhaarman
3
尽管你没有使用“FragmentManager”,但Honeycomb系统肯定在使用它。这是否发生在真正的Honeycomb平板电脑上?或者可能是有人在手机上运行了一个被黑客入侵的Honeycomb版本,导致出现了问题? - CommonsWare
1
我不知道。这是我在市场开发者控制台中得到的唯一信息,用户消息也没有任何有用的信息。 - nhaarman
这是我的解决方案: https://dev59.com/A-o6XIcBkEYKwwoYOR0q#31016553 希望有人能解决这个问题。 - nobjta_9x_tq
对我来说最好的工作答案是这个:https://dev59.com/dpjga4cB1Zd3GeqPNKNh#62554599(也不会损失状态) - Beko
显示剩余4条评论
36个回答

774
请查看我的答案这里。基本上,我只需要:
@Override
protected void onSaveInstanceState(Bundle outState) {
    //No call for super(). Bug on API Level > 11.
}

不要在saveInstanceState方法中调用super(),这会导致问题出现...
这是支持包中已知的一个bug
如果您需要保存实例并向outState Bundle添加内容,可以使用以下方法:
@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE");
    super.onSaveInstanceState(outState);
}

最终正确的解决方案是(如评论中所示)使用:
transaction.commitAllowingStateLoss();

在添加或执行导致异常的FragmentTransaction时。

406
你应该使用commitAllowingStateLoss()而不是commit()。 - meh
21
这条评论关于commitAllowingStateLoss()的内容已经可以作为一个完整的回答发布。建议您将其发布为一个独立的回答。 - Risadinha
20
关于 'commitAllowingStateLoss' --/> "这是危险的,因为如果活动需要从其状态中恢复,那么提交可能会丢失,因此只应在可以接受UI状态意外更改的情况下使用它。" - Codeversed
12
如果我查看popBackStackImmediate的v4源代码,如果状态已被保存,它会立即失败。之前使用commitAllowingStateLoss添加片段并没有起任何作用。我的测试表明这是真实的,对于这个特定的异常没有影响。我们需要一个popBackStackImmediateAllowingStateLoss方法。 - Synesso
3
@DanieleB 是的,我在这里发布了一个答案。但是我实际上通过使用Otto消息总线找到了一种更好的解决方案:将碎片注册为订阅者,并从总线中侦听异步结果。暂停时取消注册,恢复时重新注册。异步还需要一个生产方法,以处理在完成异步操作且片段已暂停时的情况。等我有时间时,我会更详细地更新我的答案。 - Synesso
显示剩余20条评论

148

有许多相关问题与类似的错误信息。请检查此特定堆栈跟踪的第二行。此异常与调用FragmentManagerImpl.popBackStackImmediate有关。

popBackStack一样,此方法调用如果会话状态已保存,则始终失败并出现IllegalStateException。检查源代码。无法阻止此异常抛出,因此无论如何都不能解决问题。

  • 删除对super.onSaveInstanceState的调用将无济于事。
  • 使用commitAllowingStateLoss创建片段也无济于事。

以下是我观察到该问题的方式:

  • 有一个带有提交按钮的表单。
  • 单击按钮后,将创建对话框并开始异步进程。
  • 在进程完成之前,用户点击主页键 - onSaveInstanceState被调用。
  • 进程完成后,将进行回调并尝试执行popBackStackImmediate
  • 抛出IllegalStateException

以下是我解决该问题的方法:

由于无法避免在回调中出现IllegalStateException,因此请捕获并忽略它。

try {
    activity.getSupportFragmentManager().popBackStackImmediate(name);
} catch (IllegalStateException ignored) {
    // There's no way to avoid getting this if saveInstanceState has already been called.
}

这已足以防止应用程序崩溃。但现在,用户将恢复应用程序,并看到他们认为已经按下的按钮根本没有被按下(他们认为)。表单片段仍在显示!

为了解决这个问题,在创建对话框时,请设置一些状态来指示进程已启动。

progressDialog.show(fragmentManager, TAG);
submitPressed = true;

并将此状态保存在bundle中。

@Override
public void onSaveInstanceState(Bundle outState) {
    ...
    outState.putBoolean(SUBMIT_PRESSED, submitPressed);
}

别忘了在onViewCreated中重新加载它。

然后,当恢复时,如果之前尝试过提交,则回滚片段。这可以防止用户返回到看起来未提交的表单。

@Override
public void onResume() {
    super.onResume();
    if (submitPressed) {
        // no need to try-catch this, because we are not in a callback
        activity.getSupportFragmentManager().popBackStackImmediate(name);
        submitPressed = false;
    }
}

6
有关此事的有趣阅读内容在这里:http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html。 - Pascal
如果您使用DialogFragment,我在这里提供了一个替代品:https://github.com/AndroidDeveloperLB/DialogShard - android developer
如果由Android自身调用了popBackStackImmediate会怎样? - Kimi Chiu
2
非常好。这个应该是被接受的答案。非常感谢!也许我会在popBackStackInmediate之后添加submitPressed = false;。 - Neonigma
我没有使用public void onSaveInstanceState(Bundle outState)方法。我需要为public void onSaveInstanceState(Bundle outState)设置空方法吗? - Fakhriddin Abdullaev
@FakhriddinAbdullaev,你绝对需要使用onSaveInstanceState(),因为用户可能在后台任务响应时旋转屏幕,这样你会再次遇到异常。 - Max_Payne

63

在显示片段之前,检查活动是否 isFinishing() 并注意 commitAllowingStateLoss()

示例:

if(!isFinishing()) {
FragmentManager fm = getSupportFragmentManager();
            FragmentTransaction ft = fm.beginTransaction();
            DummyFragment dummyFragment = DummyFragment.newInstance();
            ft.add(R.id.dummy_fragment_layout, dummyFragment);
            ft.commitAllowingStateLoss();
}

2
!isFinishing() && !isDestroyed() 对我不起作用。 - Allen Vork
!isFinishing() && !isDestroyed() 对我有用,但需要API 17。但它根本不显示“DialogFragment”。请参见https://dev59.com/VmUo5IYBdhLWcg3w2CWJ以获取其他好的解决方案,https://dev59.com/VmUo5IYBdhLWcg3w2CWJ#41813953帮助了我。 - CoolMind

39

现在是2017年10月,Google引入了新的Lifecycle组件来支持Android Support库。它为“无法在onSaveInstanceState之后执行此操作”这个问题提供了一些新的解决思路。

简单来说:

  • 使用Lifecycle组件确定弹出Fragment的正确时间。

详细版:

  • 为什么会出现这个问题?

    因为您正在尝试从Activity中使用FragmentManager(我猜你要承载你的Fragment吧?)来提交一个事务,为您的Fragment。通常情况下,这看起来像是您要为即将到来的Fragment执行某些事务,同时主机Activity已经调用了savedInstanceState方法(用户可能碰巧点击主屏幕按钮,因此Activity调用onStop(),在我的情况下是这个原因)。

    通常情况下,这个问题不应该发生——我们总是尝试在最开始时将Fragment加载到Activity中,例如onCreate()方法非常适合此目的。但是有时候确实会发生,特别是当您无法决定要将哪个Fragment加载到该Activity中,或者您正在尝试从AsyncTask块(或需要一些时间的任何内容)加载Fragment。在Fragment事务真正发生之前的时间,但是在Activity的onCreate()方法之后,用户可以做任何事情。如果用户按下主屏幕按钮,则会触发Activity的onSavedInstanceState()方法,这将导致can not perform this action崩溃。

    如果有人想深入了解此问题,请查看这篇博客文章。它深入了解了源代码层面,并对其进行了许多解释。此外,它还给出了您不应该使用commitAllowingStateLoss()方法来解决此崩溃的原因(相信我,它对您的代码没有任何好处)

  • 如何解决这个问题?

    • 我应该使用commitAllowingStateLoss()方法来加载Fragment吗?不,您不应该

    • 我应该重写onSaveInstanceState方法,在其中忽略super方法吗?不,您不应该

    • 我应该在活动中使用神奇的isFinishing来检查是否到了进行Fragment事务的正确时机吗?是的,这看起来似乎是正确的方法。

  • 看看Lifecycle组件能做什么。

    基本上,Google在AppCompatActivity类(及其它几个您应该在项目中使用的基类)中进行了一些实现,使其更容易确定当前生命周期状态。回顾我们的问题:为什么会出现这个问题?因为我们在错误的时间做了一些事情。所以我们尝试不去做它,并且这个问题将不复存在。

    我自己的项目中写了一点代码,以下就是我使用Life

    <code>val hostActivity: AppCompatActivity? = null // the activity to host fragments. It's value should be properly initialized.
    
    fun dispatchFragment(frag: Fragment) {
        hostActivity?.let {
           if(it.lifecyclecurrentState.isAtLeast(Lifecycle.State.RESUMED)){
               showFragment(frag)
           }
        }
    }
    
    private fun showFragment(frag: Fragment) {
        hostActivity?.let {
            Transaction.begin(it, R.id.frag_container)
                    .show(frag)
                    .commit()
        }
    </code>

    正如我上面所展示的,我将检查宿主活动的生命周期状态。使用支持库中的Lifecycle组件,这可以更加具体化。代码lifecyclecurrentState.isAtLeast(Lifecycle.State.RESUMED)表示,如果当前状态至少为onResume,不晚于它,这可以确保我的方法不会在其他生命周期状态(例如onStop)期间执行。

    • 全部完成了吗?

      当然没有。我展示的代码告诉大家一种新的方法来防止应用程序崩溃。但是,如果进入到onStop状态,那行代码将不起作用,屏幕上将不显示任何内容。当用户回到应用程序时,他们将看到一个空白屏幕,即完全没有显示任何片段的空宿主活动。这很糟糕的体验(比崩溃好点)。

      所以在这里我希望有些更好的东西:如果应用程序进入到晚于onResume的生命周期状态,它不会崩溃,事务方法是生命周期感知的;此外,当用户回到我们的应用程序时,活动将尝试继续完成该片段事务操作。

      我对此方法做了更多的补充:

    class FragmentDispatcher(_host: FragmentActivity) : LifecycleObserver {
        private val hostActivity: FragmentActivity? = _host
        private val lifeCycle: Lifecycle? = _host.lifecycle
        private val profilePendingList = mutableListOf<BaseFragment>()
    
        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        fun resume() {
            if (profilePendingList.isNotEmpty()) {
                showFragment(profilePendingList.last())
            }
        }
    
        fun dispatcherFragment(frag: BaseFragment) {
            if (lifeCycle?.currentState?.isAtLeast(Lifecycle.State.RESUMED) == true) {
                showFragment(frag)
            } else {
                profilePendingList.clear()
                profilePendingList.add(frag)
            }
        }
    
        private fun showFragment(frag: BaseFragment) {
            hostActivity?.let {
                Transaction.begin(it, R.id.frag_container)
                        .show(frag)
                        .commit()
            }
        }
    }
    

    在这个dispatcher类中,我维护一个列表,用于存储那些没有机会完成事务操作的片段。当用户从主屏幕返回并发现仍有片段等待启动时,它将进入@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)注释下的resume()方法。现在我认为它应该按照我的预期工作。


9
使用Java而不是Kotlin会更好。 - Shchvova
2
为什么你的“FragmentDispatcher”实现要使用列表来存储待处理的Fragment,如果只有一个Fragment会被恢复? - fraherm
我更喜欢这个答案。谢谢!如果使用FragmentTransactions,我们应该等到至少处于START状态,以防止崩溃。https://developer.android.com/topic/libraries/architecture/lifecycle.html#lco https://developer.android.com/topic/libraries/architecture/coroutines#suspend - Shareem Gelito Teofilo

22

这里有一个不同的解决方案。

通过使用私有成员变量,您可以将返回的数据设置为意图(intent),然后在super.onResume();之后进行处理。

代码如下:

private Intent mOnActivityResultIntent = null; 

@Override
protected void onResume() {
    super.onResume();
    if(mOnActivityResultIntent != null){
        ... do things ...
        mOnActivityResultIntent = null;
    }
 }

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
    if(data != null){
        mOnActivityResultIntent = data;
    }
}

7
根据你所做的被禁止的动作,这可能需要比onResume()更晚的时间执行。例如,如果问题出在FragmentTransaction.commit()上,那么它需要移至onPostResume()中执行。 - pjv
1
这对我来说是这个问题的答案。由于我需要将接收到的NFC标签转发到上一个活动,这就是为我所做的。 - Janis Peisenieks
7
因为我没有调用super.onActivityResult(),所以出现了这种情况。 - Sufian

21

简短且有效的解决方案:

按照以下简单步骤进行操作:

步骤

第一步:在相应的片段中覆盖onSaveInstanceState 方法,并从其中移除super方法。

 @Override
public void onSaveInstanceState( Bundle outState ) {

}  

步骤 2:在片段操作时使用fragmentTransaction.commitAllowingStateLoss( );而不是fragmentTransaction.commit( );


答案不是从其他地方复制或参考的。它是我通过多次尝试和错误得到的工作解决方案,发布出来帮助人们。 - Vinayak

14

注意:使用 transaction.commitAllowingStateLoss() 可能会给用户带来糟糕的体验。如果要了解为什么会引发此异常,请参阅此文章


9
不足以回答问题,你需要提供一个有效的答案来回答该问题。 - Umar Ata

10

我找到了一种折中的解决方案来解决这类问题。 如果出于某种原因(例如时间限制),您仍要保留您的ActivityGroups,那么您只需要实现

public void onBackPressed() {}

在你的Activity中编写一些back代码,即使在旧设备上没有这种方法,新设备也会调用该方法。


6
不要使用commitAllowingStateLoss(),它只应在UI状态意外更改对用户没有影响的情况下使用。
如果事务发生在parentFragment的ChildFragmentManager中,请在外部使用parentFragment.isResume()来检查。 https://developer.android.com/reference/android/app/FragmentTransaction.html#commitAllowingStateLoss()
if (parentFragment.isResume()) {
    DummyFragment dummyFragment = DummyFragment.newInstance();
    transaction = childFragmentManager.BeginTransaction();
    trans.Replace(Resource.Id.fragmentContainer, startFragment);
}

5

Activity.onStop() 之后不应执行片段事务! 请检查您是否有任何回调会在 onStop() 之后执行事务。更好的方法是修复问题,而不是尝试通过 .commitAllowingStateLoss() 等方式绕过该问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接