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

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()方法。现在我认为它应该像我预期的那样工作。

其他回答

将getFragmentManager()更改为getChildFragmentManager()。不要使用父FragmentManager,尽量使用self。

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

使用私有成员变量,你可以将返回的数据设置为一个意图,然后在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;
    }
}

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

@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时。

正如您在崩溃报告中看到的,引发异常的最后一行是

checkStateLoss(FragmentManager.java:1109)

如果你看checkStateLoss的实现

private void checkStateLoss() {
    if (isStateSaved()) {
        throw new IllegalStateException(
                "Can not perform this action after onSaveInstanceState");
    }
}

所以对我来说一个简单的解决方案是找到你在应用程序中调用的片段管理器的任何方法,最终导致这个方法被调用,并简单地检查是否isStateSaved()在调用该方法之前是假的。对我来说是show()方法。我是这样做的

if (!isStateSaved()) {
  myDialog.show(fragmentManager, Tag)
}

每当你试图在你的活动中加载一个片段时,请确保活动处于恢复状态,而不是进入暂停状态。在暂停状态下,您可能会丢失已完成的提交操作。

你可以使用transaction.commitAllowingStateLoss()而不是transaction.commit()来加载片段

or

创建一个布尔值并检查activity是否不进入onpause状态

@Override
public void onResume() {
    super.onResume();
    mIsResumed = true;
}

@Override
public void onPause() {
    mIsResumed = false;
    super.onPause();
}

然后在加载片段时检查

if(mIsResumed){
//load the your fragment
}