关于java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState出现原因与解决方法的几点总结
问题描述:我们在使用Fragment时常会碰见的一个错误,就是在调用Transactions.commit后,会收到一个java.lang.IllegalStateException的错误。比如这样的:
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)
我有理由相信很多人在刚开始使用Fragment是都几乎会遇见这样类似的错误,那么这篇博文就这个问题出现的原因以及解决的几个方法作一个总结。
为什么会出现这个异常?
其它我们可以在这个异常的说明上,看出一点端倪。 大概意思是:让我们尝试 commit()
一个Transactions时,要检查Activity的状态,也就是说我们不能在Activity的状态保存后去提交一个Transactions。我们通常称这种现象为Activity State Loss (活动状态丢失)。那么这个与 onSaveInstanceState()
有什么关系呢?
当我们谈论onSaveInstanceState()
时,我们在谈论什么?
这个函数执行时,Activity到底做了什么?我们都知道每个Activity都有自己的生命周期,都会执行自己的生命周期函数。这个是Android架构决定的。
我们知道,当Activity调用这个方法时,会传一个Bundle对象,那么这个对象就是用来 保存Activity的各个状态的。包括了属于它的Dialog、Fragment以及各个Views的状态。当然这些都是系统自己处理的。当系统重建这个Activity时,就可以还原销毁之前保存的各个状态。
既然如此,那怎么还会出现这个异常呢?Android给出了解释:
这个问题源于这样的事实,Bundle对象代表一个Activity在调用onSaveInstanceState()方法的一个瞬间快照,仅此而已。这意味着,当你在onSaveInstanceState()方法调用后会调用FragmentTransaction的commit方法。这个transaction将不会被记住,因为它没有在第一时间记录为这个Activity的状态的一部分。从用户的角度来看,这个transaction将会丢失,可能导致UI状态丢失。为了保证用户的体验,Android不惜一切代价避免状态的丢失。因此,无论什么时候发生,都将简单的抛出一个IllegalStateException异常。
何时会出现这个异常?
或许你在遇到这个问题的时候不尽相同,那么有些人就会自己臆断说:其实这是Support Library自身的bug,或许是,或许也不是。确切地说其实是与平台相关,不现版本的平台有细微的差别。这些细微区别存在的原因是源于Honeycomb上对于Activity生命周期所做的巨大改变。
在Honeycomb之前,Activity直到暂停后才考虑被销毁。这意味着在onPause()方法之前
onSaveInstanceState()
方法被立即调用。然而,从Honeycomb开始,考虑销毁Activity只能是在他们停止之后,这意味着onSaveInstanceState()
方法现在是在onStop()
方法之前调用,以此代替在onPause()
方法之前调用。
这些不同总结如下表:
说明 | Honeycomb之前的版本 | Honeycomb及更新的版本 |
---|---|---|
Activity在onPause() 调用前结束 | N | N |
Activity在onStop() 调用前结束 | Y | N |
onSaveInstanceState() 执行时机 | onPause() 前 | onStop() 前 |
由于不同版本的Activity生命周期管理存在细微的差别,那么Support Library在不同版本上的实现也就会随着系统版本的变动而做出相应的变动。比如,在Honeycomb及以上的设备中,每当commit()
在onSaveInstanceState()
之后执行时,都会抛出一个异常来提醒开发者状态丢失了。然而,在Honeycomb之前的设备上,它发生并抛出异常将更受限制,因为他们的onSaveInstanceState()
在Activity的生命周期中更早调用,结果更容易发生状态丢失。 Support Library在不同版本的行为总结如下表:
说明 | Honeycomb之前的版本 | Honeycomb及更新的版本 |
---|---|---|
commit() 在onPause() 前调用 | OK | OK |
commit() 在onPause() 与onStop() 间调用 | LOSS | OK |
commit() 在onStop() 后调用 | EXCEPTION | EXCEPTION |
避免异常出现的几点建议
俗话说:知其然,知其所以然。当我们真正了解了问题的出现的原因后,想必解决问题也就会轻松很多。所以以后我们在使用Transactions时,一定要牢记下面的几点建议:
Tips 1 不要轻易在onCreate()
以外的生命周期函数内执行commit()
在Activity的生命周期函数内commit()
Transactions时,千万要小心。当然我们大部分时候是在onCreate()
这个周期内使用,这并没有什么问题。但是,我人不少会碰见在诸如:如onActivityResult()
、onStart()
和onResume()
,特别是onActivityResult()
内commit()
的情况,这个时候,当我们认为很安全的时候,事情往往出乎意料地变得微妙起来。因为某引起函数可以在Activity的状态恢复前被调用。这个时候Activity还是处于State Loss的状态,问题往往又会如同幽灵般的出现了。当然,如果必须要在这些时候调用的,Android也给出了一些妥协的解决办法:
如果你的应用要求在除
onCreate()
函数之外的其他Activity生命周期函数中提交transaction,你可以在FragmentActivity的onResumeFragments()
函数或者Activity的onPostResume()
函数中提交。这两个函数确保在Activity恢复到原始状态之后才会被调用,从而避免了状态丢失的可能性。
比如上面我提到的Activity的onActivityResult()
方法内提交Transactions的需求,我在StackOverflow中找到个一个很好的回答,大家如果有遇到这个问题,可以去看看。
Tips 2 避免在异步回调函数中不安全地提交Transactions
比如AsyncTask的onPostExecute()
方法和LoaderManager.LoaderCallbacks的onLoadFinished()
方法。因为在这些方法内,完全没有Activity生命周期的当前状态。一个有趣的事件序列:
- 一个Activity执行一个AsyncTask。
- 用户按下“Home”键,导致Activity的
onSaveInstanceState()
和onStop()
方法被调用。- AsyncTask完成并且onPostExecute方法被调用,而它没有意识到Activity已经结束了。
- 在
onPostExecute()
函数中提交的FragmentTransaction,导致抛出一个异常。
所以,避免的方法就是将在源头上杜绝,永远不要在不要在异步回调函数中不安全地提交Transactions。当然这也不是我说的,Google的工程师也青睐这种看法。我无意中在Android Developers group上看到一篇博文却是一个佐证。当然我同样在StackOverflow上看到了问题1,问题2回答,从使用的角度也证明了这一点。
Tips 3 使用commitAllowingStateLoss()
函数
作为最后一个万不得已的时候,才能使用的方法,与commit()
的唯一区别就是当发生状态丢失的时候,前者不会抛出一个异常。通常来讲我们不应该使用这个函数,因为它很可能会引起状态丢失。
写在最后
写了这么多,也重新认识了Transactions与Activity状态之间的微妙关系,到最后来也就是两点:
commit()
函数确保在Activity的状态保存之前调用- 除非状态丢失的可能无可避免,否则就不应该使用
commitAllowingStateLoss()
函数。
鸣谢