我在市场上从我的应用程序获得用户报告,交付以下异常:

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有关,而我并不使用它。堆栈跟踪没有显示任何我自己的类,所以我不知道这个异常发生在哪里以及如何防止它。

为了记录:我有一个tabhost,在每个选项卡中都有一个在活动之间切换的ActivityGroup。


当前回答

这是2017年10月,谷歌制作了Android支持库与新事物称为生命周期组件。它为“onSaveInstanceState后不能执行此操作”的问题提供了一些新的思路。

简而言之:

使用生命周期组件来确定弹出片段的时间是否正确。

带解释的较长版本:

why this problem come out? It's because you are trying to use FragmentManager from your activity(which is going to hold your fragment I suppose?) to commit a transaction for you fragment. Usually this would look like you are trying to do some transaction for an up coming fragment, meanwhile the host activity already call savedInstanceState method(user may happen to touch the home button so the activity calls onStop(), in my case it's the reason) Usually this problem shouldn't happen -- we always try to load fragment into activity at the very beginning, like the onCreate() method is a perfect place for this. But sometimes this do happen, especially when you can't decide what fragment you will load to that activity, or you are trying to load fragment from an AsyncTask block(or anything will take a little time). The time, before the fragment transaction really happens, but after the activity's onCreate() method, user can do anything. If user press the home button, which triggers the activity's onSavedInstanceState() method, there would be a can not perform this action crash. If anyone want to see deeper in this issue, I suggest them to take a look at this blog post. It looks deep inside the source code layer and explain a lot about it. Also, it gives the reason that you shouldn't use the commitAllowingStateLoss() method to workaround this crash(trust me it offers nothing good for your code) How to fix this? Should I use commitAllowingStateLoss() method to load fragment? Nope you shouldn't; Should I override onSaveInstanceState method, ignore super method inside it? Nope you shouldn't; Should I use the magical isFinishing inside activity, to check if the host activity is at the right moment for fragment transaction? Yeah this looks like the right way to do. Take a look at what Lifecycle component can do. Basically, Google makes some implementation inside the AppCompatActivity class(and several other base class you should use in your project), which makes it a easier to determine current lifecycle state. Take a look back to our problem: why would this problem happen? It's because we do something at the wrong timing. So we try not to do it, and this problem will be gone. I code a little for my own project, here is what I do using LifeCycle. I code in Kotlin.

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()
    }

如上所示。我将检查主机活动的生命周期状态。对于支持库中的生命周期组件,这可能更具体。代码lifecyclecurrentState.isAtLeast(lifeccycle . state . resumed)的意思是,如果当前状态至少是onResume,不晚于它?这确保我的方法不会在其他生命状态(如onStop)期间执行。

Is it all done? Of course not. The code I have shown tells some new way to prevent application from crashing. But if it do go to the state of onStop, that line of code wont do things and thus show nothing on your screen. When users come back to the application, they will see an empty screen, that's the empty host activity showing no fragments at all. It's bad experience(yeah a little bit better than a crash). So here I wish there could be something nicer: app won't crash if it comes to life state later than onResume, the transaction method is life state aware; besides, the activity will try continue to finished that fragment transaction action, after the user come back to our app. I add something more to this method:

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(lifeccycle . event . on_resume)注释下的resume()方法。现在我认为它应该像我预期的那样工作。

其他回答

我找到了一个解决这类问题的下流方法。如果您出于某种原因(我有时间限制的原因)仍然希望保留ActivityGroups,您只需实现即可

public void onBackPressed() {}

在Activity中做一些回显代码。即使旧设备上没有这样的方法,这个方法也会被新设备调用。

请在这里检查我的答案。基本上我只需要:

@Override
protected void onSaveInstanceState(Bundle outState) {
    //No call for super(). Bug on API Level > 11.
}

不要在saveInstanceState方法上调用super()。这把事情搞砸了……

这是支持包中的一个已知错误。

如果你需要保存实例并添加一些东西到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时。

另一个可能的解决方法,我不确定是否在所有情况下都有帮助(来源在这里):

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        final View rootView = findViewById(android.R.id.content);
        if (rootView != null) {
            rootView.cancelPendingInputEvents();
        }
    }
}

Fragment事务不应该在Activity.onStop()之后执行! 检查你没有任何回调可以在onStop()之后执行事务。最好是修复原因,而不是尝试使用.commitAllowingStateLoss()等方法来绕过问题。

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

这个方法调用,就像popBackStack一样,如果会话状态已经被保存,它总是会以IllegalStateException失败。检查来源。您无法阻止抛出此异常。

删除对super的调用。onSaveInstanceState没有帮助。 使用commitAllowingStateLoss创建Fragment是没有帮助的。

以下是我对这个问题的观察:

有一个带有提交按钮的表单。 当单击按钮时,将创建一个对话框并启动异步进程。 用户在过程结束之前单击home键—调用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;
    }
}