原文地址:http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
作者:Alex Lockwood 3013年8月20日
Honeycomb首版发布以来,以下堆栈跟踪和异常消息就一直困扰着StackOverflow
<span style="font-size:18px;">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)</span>
本文将解释为什么和什么时候会出现这个异常,并提出一些建议,以便您的应用程序再也不会因为这个异常崩溃。
为什么会出现这个异常?
在Activity状态保存后,如果你试图提交一个FragmentTransaction,就会出现该异常,从而会造成Activity状态丢失。在我们详细讨论这意味着什么之前,让我们先来看看onSaveInstanceState()被调用之后会发生什么。在我的上一篇文章Binders &Death Recipients中,我提到,在Android运行时环境中,Android应用很难控制自身的命运。Android系统可以在任何时候终止进程以释放内存,作为结果,几乎没有任何警告,后台activity就被杀死了。为了保证这种有时候不稳定的行为不影响到用户,Android框架允许Activity在销毁之前通过调用onSaveInstanceState()来保存状态。当恢复被保存的状态时,不管Activity是否被系统杀死过,用户都会以为是在前台和后台进程中无缝切换。
系统调用onSaveInstanceState()时,会向这个方法传递一个Activity的Bundle对象来保存Activity状态,Activity会把dialog、fragment、view的状态记录在这个Bundle对象中。当方法返回时,系统通过Binder接口,把Bundle对象安全的存储到System Server 进程中。当系统重新创建Activity时,向应用程序发送同一个Bundle对象,用来恢复Activity的旧状态。
那为什么会抛出这个异常呢?这个问题源于一个事实,即这些Bundle对象只是Activity在调用onSaveInstanceState()之后一个时刻的快照,仅此而已。也就是说当你调用onSaveInstanceState()之后再调用FragmentTranceState()#commit()时,这个transaction不会被记录,因为从一开始就没有把它当成Activity状态的一部分。从用户的角度来看,这个transaction会丢失,从而导致意外的UI状态丢失。但是为了保证用户体验,Android系统不惜一切代价避免状态丢失,所以当状态丢失出现时只是简单的抛出一个IllegalStateException异常。
什么时候会出现这个异常?
如果你遇到过这个异常,你可能已经注意到,不同平台版本之间出现这个异常的时机略有不同。例如,你可能发现旧设备不经常抛出这个异常,或者当你使用support library时,比使用官方框架更容易崩溃。这些微小的不一致让很多人认为support library有错误,不能信任这些support library。但是事实并非如此。
这些微小的不一致源于Honeycomb.Prior到Honeycomb的升级,这次升级对Activity生命周期做了很大的调整。Honeycomb之前,只有Activity暂停后才能被杀死,也就意味着onSaveInstanceState()在紧邻onPause()之前被调用,然而从Honeycomb开始,Activity只有在停止后才能被杀死,也就是说,现在,onSaveInstanceState()会紧邻onStop()之前被调用,而不是紧邻onPause()之前。这些差异如下表所示:
pre-Honeycomb | post-Honeycomb | |
---|---|---|
Activities can be killed before onPause() ? |
NO | NO |
Activities can be killed before onStop() ? |
YES | NO |
onSaveInstanceState(Bundle) isguaranteed to be called before... |
onPause() |
onStop() |
对生命周期的改动,导致support library有时候需要根据平台版本改变其行为。例如,在Honeycomb及以上设备中,每次调用onSaveInstanceState()之后再调用commit(),都会抛出一个异常来警告开发者发生了状态丢失。但是,对于Honeycomb之前的设备来说,每次都抛出一个异常是过于严格了,这类设备在Activity中更早的调用了onSaveInstanceState(),也更容易产生状态丢失。Android团队被迫做出妥协:为了更好地与旧版本的平台交互,旧版本不得不接受onPause()和onStop()之间产生的意外状态丢失。两个平台的support
library行为总结如下:
pre-Honeycomb | post-Honeycomb | |
---|---|---|
commit() before onPause() |
OK | OK |
commit() between onPause() and onStop() |
STATE LOSS | OK |
commit() after onStop() |
EXCEPTION | EXCEPTION |
如何避免这个异常?
一旦你明白真正是怎么回事,避免Activity状态丢失就相当容易了。如果你已经明白了上面所讲的,希望你对support library如何工作以及它为什么对避免状态丢失如此重要也了解一些。或许你在这篇文章中只想找到快速的解决方法,但是,有一些建议,希望你在使用FragmentTransactions的时候能记在脑海里:
- 在Activity生命周期方法中commit transaction的时候要小心。
大部分应用只会在onCreate()方法首次被调用,和/或响应用户输入时commit transaction,这样不会出现任何问题。但是,当你的transaction进入其他的Activity生命周期,例如,onActivityResult(),onStart(),onResume(),事情就有点棘手了。例如,你不应该在FragmentActivity#onResume() 方法中commit transaction,因为有时候会在Activity状态恢复之前调用onResume()(参考文档)。如果你的应用需要在onCreate()以外的生命周期方法中commit
transaction,把它放在FragmentActivity#onResumeFragments()或者Activity#onPostResume()中。这两个方法都能保证在Activity状态恢复之后被调用,从而避免了状态丢失。(例子:点击打开链接)
- 避免异步回调方法内部执行transaction。
包括常用的AsyncTask#onPostExecute()和LoaderManager.LoaderCallback#onLoadFinished()。在这些方法中执行transaction的问题是,在他们被调用时不知道Activity生命周期的当前状态。例如,考虑下面两个事件的顺序:
- Activity执行一个AsyncTask
- 用户按下“Home”键,onSaveInstanceState()和onStop()被调用。
- AsyncTask执行完毕,onPostExecute()被调用,不知道Activity已经停止。
- onPostExecute()中的一个FragmentTransaction被commit,导致异常抛出。
通常,避免这个异常的最好的方法是,避免在异步回调方法中commit transaction。谷歌工程师似乎也同意这种观点。发表在Android Developers group上的这篇文章中,Android团队认为那些因在异步回调方法中commit
FragmentTransaction而导致的UI切换产生了不好的用户体验。如果你的应用需要在回调方法中执行transaction并且没有简单地方法保证紧跟着onSaveInstanceState()调用callback,那么你需要借助commitAllowingStateLoss(),并且处理可能出现的状态丢失(查看StackOverflow上的这两篇文章来获取更多提示,文章一,文章二)
- 把commitAllowingStateLoss()放到最后考虑。
调用commit()和commitAllowingStateLoss()的唯一不同点是,如果出现状态丢失,后者不会抛出异常。通常你不会想使用这种方法,因为它意味着状态丢失的可能。当然,更好的办法是在你的应用程序中保证在Activity状态保存之前调用commit(),这样才会有好的用户体验。除非不能避免状态丢失,否则不应该使用commitAllowingStateLoss()
希望这些提示能帮助你解决过去你遇到的关于这个异常的问题,如果仍有疑问,请在StackOverflow上提出问题,在本文的评论中贴出链接,我会查看的 :)