Fragment Commit 异常处理

自从Honeycomb发布后,像下面这个栈信息经常会在StackOverFlow中出现

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1327)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1338)
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在onPause()之前不会被杀掉,这意味着onSaveInstanceState()会在onPause()之前被调用。而从HoneyComb开始,Activity只会在OnStop()后被杀掉,这意味着onSaveInstanceState()会在onStop()之前被调用而不是在onPause()之前。这些变化在下表中总结:

知识点Honeycomb之前Honeycomb之后
Activity是否可以在onPause()之前被杀掉?NONO
Activity是否可以在onStop()之前被杀掉?YESNO
onSaveInstanceState(Bundle) 保证在…之前被调用onPause()onStop()

##怎么避免这个异常?

  • 在Activity生命周期方法中commit transation的时候一定要小心。
    很多应用程序只会在onCreate()或者为了响应用户输入的时候调用一次,所以他们不会遇到任何问题。
    然而,当你的transation在其他的生命周期比如onActivityResult(),onStart(),onResume() 中commit的时候,事情就可能变得棘手了。
    比如,你不应该在FragmentActivity#onResume() 方法中commit transation,为了避免这个方法在Activity的状态恢复之前被调用。
    如果你的应用程序需要在处理onCreate()之外的生命周期方法中commit transation,建议在FragmentActivity#onResumeFragments() 或者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()就不应该被使用。

##小结
Commit FragmentTransaction ----> activity onSaveInstanceState(Bundle)(Activity状态保存) Right

OnResume() Called ----> activity restored(Activity被恢复) —> Commit FragmentTransaction Right

用图形象表示如下:
此处输入图片的描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值