Fragment Transactions & Activity State Loss

原文:http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html

下面的异常信息从蜂巢开始出现:

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的状态被保存后执行一个fragment事务,结果导致一个已知的的现象--activity状态丢失.在我们了解这实际上意味着什么之前,无论如何,让我们先看一下当onSaveInstanceState被调用时发生了什么.由于在我最近的一篇关于Binders & Death Recipients专栏文章中已经讨论过,android应用很少在运行环境里管理它们的的生命周期.android系统能够在任何时候终止进程来释放内存,后台activity可能会在几乎没有警告提示下被终结.为了确保这种有时奇怪的行为不让用户看到,系统框架给了每个activity一个机会在activity易于被破坏之前去调用onSaveInstanceState.当这种保存的状态稍后被存储,用户感觉应用在前后台activity中间无缝切换,避免activity被系统终结.

当系统框架调用onSaveInstanceState()时,它为activity传递一个Bundle来保存状态,并且activity会在bundle里记录dialog/fragment/views的状态.当方法返回结果时,系统会通过Binder接口打包Bundle给系统服务进程安全地保存起来.当系统稍后重写创建Acitivity时,它把保存的Bundle返回给应用,这样来储存Acitivity的旧状态.

因此为什么这个异常会被抛出?这个问题是由于这些Bundle对象展示了一个activity在onSaveInstanceState()被调用时的情况,没有别的更多信息.这意味着当你在onSaveInstanceState()之后调用FragmentTransaction#commit()时,因为这个事务在activity的状态保存部分没有被记录,所以它不会执行.从用户的视角看,这个执行会丢失,结果导致意外的UI状态丢失.为了确保用户体验,android在这个问题发生时通过简单地抛出IllegalStateException异常来避免状态丢失造成的影响.

 

什么时候这个异常会被抛出?


如果你之前遇到过这个异常,你可能已经注意到在不同的android平台版本这个异常被抛出的情况有一点小的区别.例如,你可能已经发现老版本的设备不会频繁的抛出异常,或者当你的应用使用了支持的库会比使用正式的框架类更频繁抛出异常.这些轻微的区别让许多人认为支持库有bug,不可靠.然而这些假设都不对.

这些轻微的区别对activity生命周期有重要英雄的原因发生在Honeycomb. Prior到Honeycomb,Acitivity直到pause才被终结,这意味着onSaveInstanceState()在onPause()之前立马被调用.从Honeycomb开始,activity直到stop才被终结,这意味着onSaveInstanceState()现在将在onStop()之前调用.这些不同总结在下面的表格中:

 pre-Honeycombpost-Honeycomb
Activities can be killed before onPause()? NO NO
Activities can be killed before onStop()? YES NO
onSaveInstanceState(Bundle) is guaranteed to be called before... onPause() onStop()

 

由于这些轻微的区别对activity生命周期的影响,支持库有时需要根据平台版本改变它的行为.例如,在Honeycomb或更高版本的设备,每次在onSaveInstanceState()之后调用commit()都会抛出一个异常来警告开发者:状态丢失已经发生.然而,这种每次都会抛出异常的现象在Honeycomb之前有太多的限制,因为它们的onSaveInstanceState()在activity生命周期中被调用的更早,更容易发生状态丢失.android团队被迫妥协:为了在平台上更好地运行老版本,老设备不得不接受意外的状态丢失在onPause()和onStop()之间.支持库根据两只平台的行为总结在下面的表格中:

 pre-Honeycombpost-Honeycomb
commit() before onPause() OK OK
commit() between onPause() and onStop() STATE LOSS OK
commit() after onStop() EXCEPTION EXCEPTION

 

 

怎么避免这个异常?

 

一旦你理解了为什么activity状态丢失会发生,避免它的出现就变得简单了.如果你在这篇文章已经理解这点,希望你能更好的理解支持库怎么运转和为什么在你应用中避免状态丢失这么重要.以防你是搜索快速修复bug找到这篇文章,这里有一些建议来帮助你在应用中使用FragmentTransactions.

  • 在activity生命周期方法中使用commit时要小心.大多应用只在onCreate()中执行transaction或者响应用户的输入,这不会有人和问题.然而,由于你的应用冒险在activity的其他生命周期,比如onActivityResult()/onStart()/onResume()中调用commit,事情就有一点麻烦.例如,你不应该在onResume里执行事务,因为有一些方法能在activity的状态保存前调用.如果你的应用需要在activity除了onCreate()的其他生命周期中执行事务,尝试在onResumeFragments()和onPostResume()中执行.这两个方法确保在activity状态恢复到它的原始状态之后调用,因此能im状态丢失出现的可能性.(至于它怎么实现的,查看我关于这个问题的回答,有一些想法关于怎样在onActivityResult方法中执行fragment事务.)

  • 避免在异步回调中执行事务操作.This includes commonly used methods such asAsyncTask#onPostExecute() and LoaderManager.LoaderCallbacks#onLoadFinished(). The problem with performing transactions in these methods is that they have no knowledge of the current state of the Activity lifecycle when they are called. For example, consider the following sequence of events:

    1. An activity executes an AsyncTask.
    2. The user presses the "Home" key, causing the activity's onSaveInstanceState() and onStop() methods to be called.
    3. The AsyncTask completes and onPostExecute() is called, unaware that the Activity has since been stopped.
    4. FragmentTransaction is committed inside the onPostExecute() method, causing an exception to be thrown.

    In general, the best way to avoid the exception in these cases is to simply avoid committing transactions in asynchronous callback methods all together. Google engineers seem to agree with this belief as well. According to this post on the Android Developers group, the Android team considers the major shifts in UI that can result from committing FragmentTransactions from within asynchronous callback methods to be bad for the user experience. If your application requires performing the transaction inside these callback methods and there is no easy way to guarantee that the callback won't be invoked after onSaveInstanceState(), you may have to resort to usingcommitAllowingStateLoss() and dealing with the state loss that might occur. (See also these two StackOverflow posts for additional hints, here and here).

  • 把使用commitAllowingStateLoss()作为最后的依靠. The only difference between calling commit() andcommitAllowingStateLoss() is that the latter will not throw an exception if state loss occurs. Usually you don't want to use this method because it implies that there is a possibility that state loss could happen. The better solution, of course, is to write your application so that commit() is guaranteed to be called before the activity's state has been saved, as this will result in a better user experience. Unless the possibility of state loss can't be avoided,commitAllowingStateLoss() should not be used.

Hopefully these tips will help you resolve any issues you have had with this exception in the past. If you are still having trouble, post a question on StackOverflow and post a link in a comment below and I can take a look. :)

As always, thanks for reading, and leave a comment if you have any questions. Don't forget to +1 this blog and share this post on Google+ if you found it interesting!

posted @ 2014-09-29 18:19  范范小人物  阅读(206)  评论(0)    收藏  举报