1. 问题描述:
自从Android Honeycomb发布以来,下面的异常信息和trace已经在StackOverflow提过很多次了:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1341)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1352)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:574)
这篇博文会解释这个异常信息为什么会产生,什么时候会被抛出。并且通过提供几个建议确保你的App再也不会因此而挂掉。
2.为什么会产生这个异常
当Activity状态(state)已经被保存之后,而你又试图提交一个Fragment事务,系统就会抛出这个异常,直接导致的现象就是Activity的状态丢失了。在我们深入问题之前,让我们先看一下当onSaveInstanceState()
被调用时系统做了什么。就像我以前博文Binders & Death Recipients里所说的,Android应用程序很难控制自己的命运。Android有能力在任何时候干掉进程去释放内存,并且后台活动可能毫无预兆地被干掉。为了确保用户有时的意外行为,系统框架通过在Actvitiy被干掉之前调用onSaveInstanceState()
方法去保存Activity的状态。当保存的状态被还原后,用户能够在切换前台和后台活动时感受到无缝切换,而不用考虑活动是否已经被系统干掉了。
当系统调用onSaveInstanceState()
方法时,系统传递了一个Bundle
对象好让Activity能够保存它的状态,之中Activity保存的状态包括对话框、Fragment和各种视图等。当这个方法返回后,系统会序列化Bundle
对象通过Binder接口安全的存放在系统进程中。当系统决定重新创建这个Activity后,系统会发送同样的Bundle
到这个应用,被用来还原Acitivity的老的状态。
那么为什么会抛出这个异常呢?这个问题的实质就是当Activity调用onSaveInstanceState()
方法后,Bundle
对象保存了这个Activity的“快照”。这就意味着当你在onSaveInstanceState()
被调用之后调用的FragmentTransaction#commit()
,因为Activity已经保存了当前的状态并且不会记录这个事务,这个事务的提交就不会被记住。从用户的角度来看,这个事务会被丢掉,导致UI的状态丢失。为了保证用户体验,Android不惜一切避免状态丢失,当它发生的时候,就直接爆出一个IllegalStateException异常。
3.什么时候会抛出这个异常
如果你以前遇见过这个异常,或许你已经注意到不同版本平台异常抛出的时间略有不同。例如,你可能发现老设备更少地抛出这个异常,或者你的应用更可能crash在使用support库而不是官方系统库。这就导致了很多人控诉support库有bug并且不可信。事实上,这些控诉都是不正确的。
这些不一致的现象的原因是因为Honeycomb版本对Activity的生命周期做了重大的修改。在Honeycomb之前,Activity在被暂停之前是不可被杀的,这意味着onSaveInstanceState()
是在onPause()
之前调用的。自从Honeycomb版本,Activity只能在停止以后是可被杀的,意味着onSaveInstanceState()
会在onStop()
之前调用,而不是在onPause()
之前调用。总结如下:
Honeycomb之前 | Homeycomb之后 | |
Activity能在onPause() 之前被干掉 | NO | NO |
Activity能在onStop() 之前被干掉 | YES | NO |
onSaveInstanceState(Bundle) 通常在哪个方法之前调用? | onPause() | onStop() |
因为Activity生命周期的改变,support库有时需要根据平台版本的不同而改变它的行为。例如,设备运行在Honeycomb或以上的系统,每次在onSaveInstanceState()
后调用commit()
方法,系统都会抛出异常去警告开发者发生了状态丢失。然而,每次都抛出一个异常是过于严格的在设备运行Honeycomb以前的系统,它们的onSaveInstanceState()
在Activity生命周期中被更早的调并且更容易导致状态丢失。Android团队不得不做出了妥协:在了在老系统上拥有更好的交互操作,在onPause()
和onStop()
之间,老设备只能承受状态丢失。Support库在不同版本上不同的行为可总结为:
Honeycomb之前 | Homeycomb之后 | |
commit() 在onPause() 之前 | OK | OK |
commit() 在onPause() 和onStop() 之间 | STATE_LOSS | OK |
commit() 在onStop() 之后 | EXCEPTION | EXCEPTION |
4.如何避免这个异常?
一旦你理解了这个异常的实质情况,避免这个异常就变的非常简单了。如果你已经做到了这一步,希望你能理解Support是怎样工作的,并且为什么在应用中避免状态丢失是如果的重要。如果你只是在查找快速解决方法,下面是一些关于使用FragmentTransaction的建议,在日后的工作中牢记在心:
(4.1)在Activity生命周期方法提交事务要格外小心。
绝大多数的应用在onCreate()
方法的时候或者用户操作的时候就提交了事务,所以不会出现问题。但是,当你在Activity其他生命周期方法中调用事务,如onActivityResult()
、onStart()
和onResume()
方法等,事情就变的不那么美好了。例如,你不应该在FragmentActivity#onResume()
方法中提交事务,因为有时候这个方法会在Activity还原状态之前被调用(详情见文档))。如果你的应用真的需要在Activity生命周期非onCreate()
方法中提交事务,请在FragmentActivity#onResumeFragments()
或Activity#onPostResume()
方法中提交。这两个方法会在Activity还原状态之后被调用,因此可以避免状态丢失(一个例子关于怎么使用,请看我对这个问题的的答案,里面提供了一些关于在Activity#onActivityResult()
方式中提交FragmentTransaction的想法。)。
(4.2)在异步回调方法中避免调用事务。
这里通常包括了像AsyncTask#onPostExceute()
和LoaderManager.LoaderCallback#onLoadFinished()
方法。在这里调用事务的问题在于当这些方法被调用时他们并不知道当前Activity生命周期状态。例如,考虑一下的步骤:
- 一个Activity启动了一个AsyncTask。
- 用户按下了Home键,导致这个Activity的
onSaveInstanceState()
和onStop()
方法被调用。 - 这个AsyncTask完成了并且调用了onPostExecute()方法,而没有意识到这个Activity已经被Stopped。
- 在onPostExecute()方法中提交了一个FragmentTransaction,而导致抛出了一个异常。 通常,避免这个异常最好的方法就是不要在所有的异步回调方法中提交事务。Google工程师好像也坚定这个信念。根据在Google Group上的关于Android开发的这篇博文,Android团队认为在异步回调方法中提交FragmentTransaction并转变UI是有损用户体验的。如果你应用需要在异步回调方法中调用事务,并且也没有简单的方法去确保回调方法不会在
onSaveInstanceState()
方法后调用,你只能用最不推荐的方法commitAllowingStateLoss()
,你还要处理可能出现的状态丢失。(想要了解更多的这部分内容,请参考这个还有这个)
(4.3)最后再考虑使用commitAllowingStateLoss()
方法。
commit()
和commitAllowingStateLoss()
唯一的不同就是,当状态丢失发生后,后者不会抛出异常。一般你不想使用这个方法是因为它意味着状态丢失会有可能发生。当然,更好的解决方法就是让你应用一定要在Activity保存状态之前调用commit()
方法,这就是更好的用户体验。出发状态丢失不能被避免,否则commitAllowingStateLoss()
不应该被使用。在使用Fragment是会遇到如下的Exception:
(4.4)我的分析:
来替换FragmentTransaction.commit()函数
@Override
protected void onSaveInstanceState(Bundle outState) {
afterOnSaveInstanceState = true;
}
在调用FragmentTransaction.commit()函数之前去判断这个变量
@Override
public void onBackPressed() {
if (!afterOnSaveInstanceState) {
super.onBackPressed();
}
}