下面自从Honeycomb发布后,下面栈跟踪信息和异常信息已经困扰了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)
这篇文章会解释这个异常什么时候会抛出以及原因,并且会以一些建议收尾。这些建议会帮助你不会因为这个异常导致程序崩溃。
这个异常为什么会抛出?
这个异常抛出的原因是因为你尝试着在Activity的状态已经保存后commit一个FragmentTransaction ,导致了一个现象叫做Activity state loss。在我们深入细节之前,让我们先看看在onSaveInstanceState()调用后发生了什么。在我的前一篇 Binders & Death Recipients有讨论到,Android应用程序在Android 运行时系统中只有很小的控制权。Android系统为了释放内存可以在任意时刻停止进程,然后处于后台的Activity就会被毫无警告地杀掉。为了保证有时候因此引起的不稳定行为能避免用户知道,Android框架给每一个Activity通过调用onSaveInstanceState()来保存自己状态的机会,它会在Activity可能被销毁之前调用。当后面恢复状态的时候,用户不会感觉到Activity已经被系统杀掉了,而会感觉前台和后台的Activity无缝切换。
当Android框架调用onSaveInstanceState(),它将一个Bundle对象通过这个方法传递,以便Activity后面恢复状态。Activity可以将它的Dialog、fragment以及view的状态保存在Bundle中。当这个方法返回的时候,系统通过Binder结果打包Bundle对象然后传给系统服务进程。系统服务进程负责保证Bundle对象安全地保存下来。当系统后面决定重新创建Activity的获释后,它就会将相同的Bundle对象发挥应用程序,以便于用它来回复旧的Activity状态。
所以为什么这个异常随后抛出?这个问题导致的原因是因为那些Bundle对象代表Activity在onSaveInstanceState()被调用那时候的一个快照,没更多了。这就意味着当你在onSaveInstanceState()之后调用FragmentTransaction#commit()的时候,transation不会被记录。因为它不会作为之前Activity的状态被保存。从用户的角度来说,这个transaction就像丢失了,导致UI状态意外的丢失。为了保证用户体验,Android不计一切代价避免状态丢失,也就是当它发生的时候简单地抛出一个IllegalStateException。
这个异常什么时候会抛出?
如果你之前已经碰到过这个异常,你可能会注意到异常抛出的时机因为不同的Android版本而不一致。比如,如可能会发现老版本的设备上,这个异常抛出比较不频繁,或者当你的程序中使用support library而不是官方框架中的类时更容易触发这个异常。这些轻微的一致让很多人都以为support library有bug,不值得信任。然而,这些假设都不是正确的。
这些轻微的不一致是因为在Honeycomb版本中的Activity生命周期有了重要的变化。Honeycomb之前的版本,activity被认为在pause之前都不会被杀掉,这意味着onSaveInstanceState()会在onPause()之前被调用。从HoneyComb开始,Activity被认为只会在stopped只会被杀掉,意味着onSaveInstanceState()现在会在onStop()之前被调用而不是在onPause()之前。这些变化在下表中总结:
Honeycomb之前 | Honeycomb之后 | |
---|---|---|
Activity是否可以在onPause()之前被杀掉? | NO | NO |
Activity是否可以在onStop()之前被杀掉? | YES | NO |
onSaveInstanceState(Bundle) 保证在...之前被调用 | onPause() | onStop() |
由于Activity生命周期的轻微变化,support library有时候需要根据系统版本选择他的行为。比如,在Honeycomb及以上设备,每次在onSaveInstanceState()之后调用commit()都会抛出一个异常,以便警告开发者已经发生了状态丢失。然而,在每次这种情况抛出异常在Honeycomb之前的设备上就显得太具有限制性了,它们的onSaveInstanceState()调用发生在Activity生命周期中更早的一段时期,并且更容易导致意外的状态丢失。Android团队被迫做出妥协:为了更好地跟老版本兼容,旧设备可能必须要忍受在onPause()和onStop()之间意外的状态丢失。Support library在不同两个版本的行为如下表总结:
Honeycomb之前 | Honeycomb之后 | |
---|---|---|
commit在onPause()之前 | OK | OK |
commit在onPause() 和onStop()之间 | STATE LOSS | OK |
commit在onStop()之后 | EXCEPTION | EXCEPTION |
怎么避免这个异常?
一旦你懂得了真正发生了什么,避免Activity状态丢失就简单多了。如果你已经在读这篇文章之间就已经解决过这个问题了,希望你能对support library有一个更深的了解,并且知道为什么避免状态丢失对你的程序这么重要。为了方便你通过这篇文章寻找快速的解决方案,这里有一些建议希望你记得在使用FragmentTransactions的时候使用:
- 在Activity生命周期方法中commit transation的时候一定要小心。很多应用程序只会在onCreate()或者为了响应用户输入的时候调用一次,所以他们不会遇到任何问题。然而,当你的transation开始冒险在其他的生命周期(比如onActivityResult(),onStart(),onResume() )中commit的时候,事情就可能变得棘手了。比如,你不应该在FragmentActivity#onResume() 方法中commit transation,为了避免有些时候这个方法在Activity的状态恢复之前被调用( 查看文档,了解更多)。如果你的应用程序需要在处理onCreate()之外的生命周期方法中commit transation,在FragmentActivity#onResume() 或者Activity#onPostResume()中调用。这两个方法会被保证在Activity恢复它的状态之后调用,因此会避免可能的状态丢失。(一个关于如何去做的例子,可以查看我在StackOverFlow上的回答。这个回答设计怎么正确地响应Activity#onActivityResult()方法,然后commit FragmentTransactions)
- 避免是异步调用方法中执行transactions 。这个包括经常被使用的方法比如AsyncTask#onPostExecute() 和 LoaderManager.LoaderCallbacks#onLoadFinished() 。在这些方法中执行transactions会有问题,因为他们当这些方法被回调的时候,他们不知道Activity当前的生命周期。比如,考虑下面的事件序列:
- 一个Activity执行一个AsyncTask
- 用户按下Home键,导致这个Activity的onSaveInstanceState()和onStop() 方法被回调。
- AsyncTask完成然后onPostExecute()被调用,而不知道Activity已经处于stopped状态。
- 在onPostExectute()方法中的FragmentTransaction被committed,导致一个异常被抛出。
总之,在这些案例中避免异常抛出的最优方法就是避免在异步回调方法中commit transactions。Google工程师似乎同意这个见解。根据在Android Develop group上的这篇文章,Android开发团队认为通过commit FragmentTransactions来让UI产生重大的变化对用户体验十分不友好。如果你的应用程序需要在这些回调方法中执行transaction,那么没有什么简单方法可以保证这些回调不会再onSaveInstanceState()后调用,你可能必须使用commitAllowStateLoss()并且处理可能发生的状态丢失。(详见两篇StackOverFlow文章,文章1、文章2)
- 只使用commitAllowingStateLoss()作为最后的解决方案。commit()和commitAllowingStateLoss()唯一的区别是后者在状态丢失的时候不会抛出异常。通常你不会想使用这个方法因为它意味着状态丢失可能发生。更好的解决方案当然是修改你的程序以便commit()被保证在activity的状态被保存前调用,因为这样可能会让用户体验更好。除非状态丢失是不可避免的,否则commitAllowingStateLoss()就不应该被使用。
希望这些建议能够帮助你解决以往因为这个异常而引起的问题。如果你还有问题,在StackOverflow上发布问题,然后将链接发表在评论区以便我能够看到。
译文链接Fragment transaction commit state loss
*:first-child {
margin-top: 0 !important;
}
body>*:last-child {
margin-bottom: 0 !important;
}
/* BLOCKS
=============================================================================*/
p, blockquote, ul, ol, dl, table, pre {
margin: 15px 0;
}
/* HEADERS
=============================================================================*/
h1, h2, h3, h4, h5, h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
}
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
font-size: inherit;
}
h1 {
font-size: 28px;
color: #000;
}
h2 {
font-size: 24px;
border-bottom: 1px solid #ccc;
color: #000;
}
h3 {
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
color: #777;
font-size: 14px;
}
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child {
margin-top: 0;
padding-top: 0;
}
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
margin-top: 0;
padding-top: 0;
}
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
margin-top: 10px;
}
/* LINKS
=============================================================================*/
a {
color: #4183C4;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* LISTS
=============================================================================*/
ul, ol {
padding-left: 30px;
}
ul li > :first-child,
ol li > :first-child,
ul li ul:first-of-type,
ol li ol:first-of-type,
ul li ol:first-of-type,
ol li ul:first-of-type {
margin-top: 0px;
}
ul ul, ul ol, ol ol, ol ul {
margin-bottom: 0;
}
dl {
padding: 0;
}
dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px;
}
dl dt:first-child {
padding: 0;
}
dl dt>:first-child {
margin-top: 0px;
}
dl dt>:last-child {
margin-bottom: 0px;
}
dl dd {
margin: 0 0 15px;
padding: 0 15px;
}
dl dd>:first-child {
margin-top: 0px;
}
dl dd>:last-child {
margin-bottom: 0px;
}
/* CODE
=============================================================================*/
pre, code, tt {
font-size: 12px;
font-family: Consolas, "Liberation Mono", Courier, monospace;
}
code, tt {
margin: 0 0px;
padding: 0px 0px;
white-space: nowrap;
border: 1px solid #eaeaea;
background-color: #f8f8f8;
border-radius: 3px;
}
pre>code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent;
}
pre {
background-color: #f8f8f8;
border: 1px solid #ccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px;
}
pre code, pre tt {
background-color: transparent;
border: none;
}
kbd {
-moz-border-bottom-colors: none;
-moz-border-left-colors: none;
-moz-border-right-colors: none;
-moz-border-top-colors: none;
background-color: #DDDDDD;
background-image: linear-gradient(#F1F1F1, #DDDDDD);
background-repeat: repeat-x;
border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
border-image: none;
border-radius: 2px 2px 2px 2px;
border-style: solid;
border-width: 1px;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
line-height: 10px;
padding: 1px 4px;
}
/* QUOTES
=============================================================================*/
blockquote {
border-left: 4px solid #DDD;
padding: 0 15px;
color: #777;
}
blockquote>:first-child {
margin-top: 0px;
}
blockquote>:last-child {
margin-bottom: 0px;
}
/* HORIZONTAL RULES
=============================================================================*/
hr {
clear: both;
margin: 15px 0;
height: 0px;
overflow: hidden;
border: none;
background: transparent;
border-bottom: 4px solid #ddd;
padding: 0;
}
/* TABLES
=============================================================================*/
table th {
font-weight: bold;
}
table th, table td {
border: 1px solid #ccc;
padding: 6px 13px;
}
table tr {
border-top: 1px solid #ccc;
background-color: #fff;
}
table tr:nth-child(2n) {
background-color: #f8f8f8;
}
/* IMAGES
=============================================================================*/
img {
max-width: 100%
}
func {
font-size: 0.95em;
color: #cc0000;
font-family: Consolas, Menlo, Monaco, ‘Lucida Console‘, ‘Liberation Mono‘, ‘DejaVu Sans Mono‘, ‘Courier New‘, monospace;
line-height: 1.3em;
}
-->