IllegalStateException:使用ViewPager执行onSaveInstanceState后无法执行此操作
我从市场上的应用程序获取用户报告,提供以下例外情况:
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有关,我不使用它。 stacktrace不显示任何我自己的类,所以我不知道发生这种exception,以及如何防止它。
logging:我有一个tabhost,并在每个选项卡中有一个ActivityGroup活动之间切换。
请在这里查看我的答案。 基本上我只是:
@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); }
最后,正确的解决scheme是(如评论所示)使用:
transaction.commitAllowingStateLoss();
当添加或执行导致Exception
的FragmentTransaction
。
有类似的错误信息有许多相关的问题。 检查这个特定的堆栈跟踪的第二行。 这个exception特别与调用FragmentManagerImpl.popBackStackImmediate
。
如果会话状态已保存,则此方法调用(如popBackStack
)将总是失败,并显示IllegalArgumentException
。 检查来源。 没有任何事情可以阻止这个exception被抛出。
- 移除对
super.onSaveInstanceState
的调用将无济于事。 - 使用
commitAllowingStateLoss
创build碎片将无济于事。
以下是我如何观察问题:
- 有一个提交button的表单。
- 当点击button时,会创build一个对话框并开始一个asynchronous过程。
- 用户在完成处理之前单击home键 –
onSaveInstanceState
被调用。 - 该过程完成后,将进行callback并尝试
popBackStackImmediate
。 -
IllegalStateException
被抛出。
以下是我所做的解决方法:
由于无法避免callback中的IllegalStateException
,因此请忽略它。
try { activity.getSupportFragmentManager().popBackStackImmediate(name); } catch (IllegalStateException ignored) { // There's no way to avoid getting this if saveInstanceState has already been called. }
这足以阻止应用程序崩溃。 但现在用户将恢复应用程序,并看到他们认为他们按下的button没有被按下(他们认为)。 表单片段仍在显示!
为了解决这个问题,当创build对话框时,使一些状态指示进程已经开始。
progressDialog.show(fragmentManager, TAG); submitPressed = true;
并将这个状态保存在包中。
@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); } }
在显示片段之前检查活动是否为commitAllowingStateLoss()
并注意commitAllowingStateLoss()
。
例:
if(!isFinishing()) { FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); DummyFragment dummyFragment = DummyFragment.newInstance(); ft.add(R.id.dummy_fragment_layout, dummyFragment); ft.commitAllowingStateLoss(); }
这是对这个问题的一个不同的解决scheme。
使用私有成员variables,您可以将返回的数据设置为可以在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; } }
简短和工作解决scheme:
遵循简单的步骤
脚步
第1步:覆盖相应片段中的onSaveInstanceState
状态。 并从中删除超级方法。
@Override public void onSaveInstanceState( Bundle outState ) { }
第2步:使用fragmentTransaction.commitAllowingStateLoss( );
而不是fragmentTransaction.commit( );
而片段操作。
注意 ,使用transaction.commitAllowingStateLoss()
可能会给用户带来不好的体验。 有关为什么引发此exception的更多信息,请参阅此帖 。
我发现这种问题肮脏的解决scheme。 如果你仍然想保持你的ActivityGroups
出于任何原因(我有时间限制的原因),你只是实现
public void onBackPressed() {}
在你的Activity
做一些back
代码。 即使在较旧的设备上没有这样的方法,这个方法也会被较新的方法调用。
不要使用commitAllowingStateLoss(),它只能用于UI状态在用户意外改变的情况。
相反,使用if(fragment.isResume())检查外部操作遇到此IllegalStateException “不能执行此操作后onSaveInstanceState”
我有类似的问题,情况是这样的:
- 我的活动是添加/replace列表片段。
- 每个列表片段都有对活动的引用,以便在列表项被点击时通知活动(观察者模式)。
- 每个列表片段调用setRetainInstance(true); 在其onCreate方法中。
活动的onCreate方法是这样的:
mMainFragment = (SelectionFragment) getSupportFragmentManager() .findFragmentByTag(MAIN_FRAGMENT_TAG); if (mMainFragment == null) { mMainFragment = new SelectionFragment(); mMainFragment.setListAdapter(new ArrayAdapter<String>(this, R.layout.item_main_menu, getResources().getStringArray( R.array.main_menu))); mMainFragment.setOnSelectionChangedListener(this); FragmentTransaction transaction = getSupportFragmentManager() .beginTransaction(); transaction.add(R.id.content, mMainFragment, MAIN_FRAGMENT_TAG); transaction.commit(); }
引发exception的原因是configuration更改(设备旋转),创build活动,从片段pipe理器的历史logging中检索主片段,同时该片段已经具有对已销毁活动的旧引用
改变这个实现解决了这个问题:
mMainFragment = (SelectionFragment) getSupportFragmentManager() .findFragmentByTag(MAIN_FRAGMENT_TAG); if (mMainFragment == null) { mMainFragment = new SelectionFragment(); mMainFragment.setListAdapter(new ArrayAdapter<String>(this, R.layout.item_main_menu, getResources().getStringArray( R.array.main_menu))); FragmentTransaction transaction = getSupportFragmentManager() .beginTransaction(); transaction.add(R.id.content, mMainFragment, MAIN_FRAGMENT_TAG); transaction.commit(); } mMainFragment.setOnSelectionChangedListener(this);
您需要在每次创build活动时设置您的侦听器,以避免碎片引用旧的活动已销毁实例的情况。
当我按下button取消我的地图片段活动上的意图select器时,我得到这个exception。 我解决了这个onResume(在我正在初始化片段)的代码replaceonstart()和应用程序工作正常。希望它有帮助。
我想使用transaction.commitAllowingStateLoss();
不是最好的解决scheme。 当活动的configuration发生变化并且onSavedInstanceState()
被调用,然后你的asynchronouscallback方法尝试提交fragment时,这个exception将被抛出。
简单的解决scheme可以检查活动是否改变configuration
例如check isChangingConfigurations()
即
if(!isChangingConfigurations()) { //commit transaction. }
检出这个链接
每当你试图在你的活动中加载一个片段时,确保活动处于恢复状态,而不是暂停状态。在暂停状态下,你最终可能会失去提交操作。
您可以使用transaction.commitAllowingStateLoss()而不是transaction.commit()来加载片段
要么
创build一个布尔值,并检查活动是否不会onpause
@Override public void onResume() { super.onResume(); mIsResumed = true; } @Override public void onPause() { mIsResumed = false; super.onPause(); }
然后加载片段检查
if(mIsResumed){ //load the your fragment }
这是2017年10月,谷歌使Android支持库与新的东西调用生命周期组件。 它为这个'onSaveInstanceState'问题后不能执行这个动作提供了一些新的想法。
简而言之:
- 使用生命周期组件来确定是否正确popup分段的时间。
更长的版本与解释:
-
为什么这个问题出来了?
这是因为你正在尝试从你的活动(这将持有你的片段,我想?)使用
FragmentManager
来为你提交一个事务片段。 通常这会看起来像你正在尝试做一些交易一个未来的片段,同时主机活动已经调用savedInstanceState
方法(用户可能碰巧主页button,所以活动调用onStop()
,在我的情况下,这是原因)通常这个问题不应该发生 – 我们总是试图在开始时加载片段,就像
onCreate()
方法是一个完美的地方。 但是有时候会发生这种情况 ,特别是当你不能确定你要加载到哪个片段,或者你试图从AsyncTask
块加载片段(或者需要一些时间)时。 在片段事务真正发生之前,但在activity的onCreate()
方法之后,用户可以做任何事情。 如果用户按下主页button,触发活动的onSavedInstanceState()
方法,将会有一个can not perform this action
崩溃。如果有人想在这个问题上看得更深,我build议他们看看这个博客文章 。 它看起来很深入的源代码层,并解释了很多。 此外,它给出的理由,你不应该使用
commitAllowingStateLoss()
方法来解决这个崩溃(相信我它没有提供任何好处的代码) -
如何解决这个问题?
-
我应该使用
commitAllowingStateLoss()
方法来加载片段吗? 不,你不应该 ; -
我应该重写
onSaveInstanceState
方法,忽略里面的super
方法吗? 不,你不应该 ; -
我是否应该使用神奇的
isFinishing
内部活动来检查主机活动是否适合碎片交易? 是的,这看起来是正确的做法。
-
-
看看生命周期组件可以做什么。
基本上,Google在
AppCompatActivity
类(以及您应该在项目中使用的其他几个基类)内部实现了一些实现,这使得更容易确定当前的生命周期状态 。 回顾一下我们的问题:为什么会出现这个问题? 这是因为我们在错误的时间做事。 所以我们尽量不要这样做,这个问题将会消失。我为自己的项目编写了一些代码,这就是我使用
LifeCycle
所做的。 我在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(Lifecycle.State.RESUMED)
意思是,如果当前状态至less是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()
方法。 现在我认为它应该像我预期的那样工作。
在你的活动中添加这个
@Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (outState.isEmpty()) { // Work-around for a pre-Android 4.2 bug outState.putBoolean("bug:fix", true); } }
从支持库版本24.0.0开始,您可以调用同步提交此事务的FragmentTransaction.commitNow()
方法,而不是先调用commit()
后再执行executePendingTransactions()
。 正如文件所说,这种方法甚至更好:
调用commitNow比调用commit()后跟executePendingTransactions()更可取,因为后者将具有尝试提交所有当前未决事务的副作用,无论这是否是所需的行为。
我也遇到过这个问题,每当你的FragmentActivity
上下文被改变时(例如屏幕方向改变等),就会出现问题。 所以最好的解决办法是从你的FragmentActivity
更新上下文。
在我的案例中,可能最顺利也是最简单的解决scheme是避免将违规的碎片从作业结果中popup堆栈。 所以改变我的onActivityResult()
这个调用:
popMyFragmentAndMoveOn();
对此:
new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { popMyFragmentAndMoveOn(); } }
在我的情况帮助。
这里抛出exception(在FragmentActivity中):
@Override public void onBackPressed() { if (!mFragments.getSupportFragmentManager().popBackStackImmediate()) { super.onBackPressed(); } }
在FragmentManager.popBackStatckImmediate()
,首先调用FragmentManager.checkStateLoss()
。 这是IllegalStateException
的原因。 看下面的实现:
private void checkStateLoss() { if (mStateSaved) { // Boom! throw new IllegalStateException( "Can not perform this action after onSaveInstanceState"); } if (mNoTransactionsBecause != null) { throw new IllegalStateException( "Can not perform this action inside of " + mNoTransactionsBecause); } }
我简单地通过使用标志来标记Activity的当前状态来解决这个问题。 这是我的解决scheme:
public class MainActivity extends AppCompatActivity { /** * A flag that marks whether current Activity has saved its instance state */ private boolean mHasSaveInstanceState; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onSaveInstanceState(Bundle outState) { mHasSaveInstanceState = true; super.onSaveInstanceState(outState); } @Override protected void onResume() { super.onResume(); mHasSaveInstanceState = false; } @Override public void onBackPressed() { if (!mHasSaveInstanceState) { // avoid FragmentManager.checkStateLoss()'s throwing IllegalStateException super.onBackPressed(); } }
}
简单而紧凑的解决scheme可能是:
@Override public void show(FragmentManager manager, String tag){ FragmentTransaction ft=manager.beginTransaction(); ft.add(this,tag); ft.commitAllowingStateLoss(); }