Fragment Transactions & Activity State Loss

自己以前在Fragment中直接增加和移除DialogFragment时,遇到了一个比较奇怪的异常。
当时找了一下相关的资料,觉得一篇英文文章分析的不错。
于是,一直想抽时间翻译并做记录。

后来自己各种拖延,加上不得已穿插做了一些其它的事,
直到今天才完成了翻译,有些汗颜。

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


自从Android 3.0版本发布以来,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的状态被系统保存后,开发人员仍试图提交一个FragmentTransaction,
于是就会导致被称为Activity State loss的现象。


在进一步了解上述现象的细节前,我们不妨先看看系统调用onSaveInstanceState时,
在底层“暗中”进行的工作。

在Android运行环境下,android应用实际上难以掌控自己的命运。
为了释放内存,Android系统有权力在任何时间杀死应用所在进程,
因此后台Activity可能在没有任何提示或警告下就被杀掉了。

为了减少系统的这种行为对用户造成的影响,Android框架在销毁Activity前,
给了Activity一个机会去调用onSaveInstanceState,以保存自己的状态。

当用户重新启动被杀死的Activity时,
之前被保存的状态将被恢复。

于是,无论Activity之前是否被系统杀死,
用户都只会有一种在前台和后台无缝切换Activity的感觉。


当Framework调用onSaveInstanceState方法时,传入了一个Bundle对象供Activity使用,
Activity会在Bundle中记录自身dialog,fragment及view的状态。

当onSaveInstanceState返回时,系统会打包Bundle对象,
并通过Binder接口将其发送给System Server进程。
由于SystemServer进程可以常驻,因此可以安全地保留Activity的状态信息。

当系统决定重新创建Activity时,
会将之前保存的包含Activity状态信息的Bundle发送给对应应用,
应用将根据Bundle中的信息恢复Activity的状态。


在了解上述原理后,我们就可以理解上述异常抛出的原因了。

当Activity调用onSaveInstanceState时,
记录状态信息的Bundle对象将作为Activity当前状态的快照。

因此,如果开发人员在onSaveInstanceState之后,
才调用FragmentTransaction的commit接口,
那么这次transaction不会被Bundle对象记录。

于是这次transaction就可能由于Activity异常终止而丢失,
即出现state loss。

为了保证用户体验,Android尽可能地避免state loss,
当出现这种情况时,就直接抛出IllegalStateException。


二、何时抛出异常

如果你也遇到过这种异常,
就可能也发现这种异常抛出的时机,
在不同的平台版本中,具有轻微的差异。

例如,你可能发现应用在旧版本的设备上抛出异常的频率较低;
或者使用支持库crash的概率,明显大于使用官方库的概率。
这些差异使得大多数用户怀疑支持库存在问题,
然而这种猜测是错误的。

造成这种差异性的真正原因是:
Android在Honeycomb版本,对Activity的生命周期进行了调整。
在Honeycomb版本之前,Activity在paused之前是无法被杀死的,
这意味者onSaveInstanceState将在onPause之前被调用。

从Honeycomb开始,Activity只有在onStop之后才能被杀死,
于是onSaveInstanceState将在onStop之前被调用,而不是onPause。

这种差异性如下表所示:

考虑到Activity生命周期在不同版本之间的差异,
支持库有时候需要根据版本调整它们的行为。

例如:在Honeycomb及之后版本的设备上,一旦Activity在onSaveInstanceState后有commit提交,
那么系统一定会抛出异常来提醒开发人员出现state loss。

然而,在Honeycomb之前的版本中,却不一定会抛出异常。

支持库在Honeycomb之前和之后版本上的表现类似于下表:

可见旧版本中,onSaveInstanceState在Activity生命周期中被调用的位置更加靠前,
更有可能受到state loss问题的影响。


三、如何避免这种异常

了解Activity state loss的原因后,避免其发生就比较简单了。
这里给出几条使用FragmentTransactions时,避免state loss的建议。

1、在Activity的生命周期函数中提交transactions时需要注意。

大多数应用,仅会在Activity的onCreate第一次被调用或响应用户输入时,
才进行提交transactions的操作。
在这种情况下,应用是不会遇到state loss这种问题。

然而,如果应用在Activity的其它生命周期函数,
例如onActivityResult、onStart和onResume中提交transaction时,
情况就变得比较微妙了。

例如,开发人员不应该在FragmentActivity的onResume函数中提交transaction。
因为在有些场景下,FragmentActivity的onResume函数将在它的状态被完全恢复前调用。

为了尽可能地避免state loss,
当应用需要在Activity的onCreate之外的生命周期函数中提交transaction时,
最好保证是在FragmentActivity的onResumeFragments函数,
或Activity的onPostResume函数中进行。

这两个方法都保证当Activity恢复了之前的状态时才被调用,
因此都能避免state loss发生。

2、避免在异步回调方法中提交transaction。

这里的异步回调方法通常指的是类似AsyncTask的onPostExecute方法、
LoaderManager.LoaderCallbacks的onLoadFinished方法等。

在这些回调函数中处理transaction的问题在于:
当函数被回调时,并不清楚Activity当前的状态。

例如,如果事件按照下列时序发生:
一个Activity启动了一个AsyncTask。

然后,在AsyncTask执行的过程中,用户点击了Home键,
导致Activity的onSaveInstanceState和onStop函数被调用。

当AsyncTask完成,并回调onPostExecute时,
并不清楚Activity已经stop了。

如果此时在onPostExecute中哦给你进行FragmentTransaction的commit操作,
就会导致异常。

因此,一般情况下为了避免这种情况发生,
应该尽量避免在异步回调函数中提交transaction。

Google官方文档似乎也认可这个观点。
在文档中提到了:在异步回调函数中,
进行FragmentTransactions的commit操作,
以带来UI的变化,将导致不好的用户体验。

如果应用一定要在异步回调函数中提交transaction,
那么没有办法保证回调函数在onSaveInstanceState之前执行。

此时,需要借助于FragmentTransaction的commitAllowingStateLoss函数,
并自己处理可能的state loss。

3、commitAllowingStateLoss仅作为最后的解决手段。

FragmentTransaction的commit和commitAllowingStateLoss函数唯一的区别是,
后者在发生state loss时不会抛出异常。

通常,开发人员不应该使用commitAllowingStateLoss,
因为这意味开发人员知道state loss有可能发生,
但没有采取积极的策略去解决。

此时,最好的解决方案应该是改写代码,
保证在Activity的state被保存前,
调用FragmentTransaction的commit方法,
这样才能带来更好的用户体验。

总之,除非state loss实在无法避免,
否则尽量不要使用commitAllowingStateLoss。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值