Fragment Transaction 和 状态丢失 state loss

翻译(加入少许例子)Alex Lockwood的文章:http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html


自从Android 3.0 Honeycomb发布以来,IllegalStateException的问题就成了一个很典型的问题: 
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)
这篇文章主要解释IllegalStateException异常出现的原因和地点,并包含如何尽量避免它的一些建议。

异常出现的原因

异常出现的原因:在Activity的状态保存之后,调用FragmentTransaction的commit(),因此可能会造成Activity state loss (Activity状态丢失)的现象。在我们深入了解它之前,首先看一下onSaveInstanceState()这个方法。由于Android应用程序并不能控制其自身的生命周期,其生命周期由Android系统根据用户操作、系统状态等来控制,Android系统可以决定终止某个进程(应用程序)来回收内存资源,而处于后台(Background)的应用程序可能会被终止。然而作为用户,当其在对App1进行了某些操作(比如输入文字)之后,切换到了App2,此时如果Android系统决定终止App1,而之前输入的内容没有被保存下来的话,就会造成用户信息的丢失,这是一种不好的用户体验。为了让Android系统对应用程序的管理对用户透明,在应用程序被(自愿)回收之前,可以在onSaveInstanceState()方法中保存其当前Activity的状态。这种情况下,就算App1被Android系统终止,当下次用户再打开此Activity之后,这些被保存的状态也可以重新加载,这样Ativity是否被Android系统回收对于用户来说就没有区别了,切换效果会和App1在foreground和background状态切换一样。

onSaveInstanceState()方法有Bundle参数,其包含了Activity中dialog、fragment、view的状态,当这个方法返回时,系统会将其通过Binder接口传到System Servier进程中,并在系统中保存。当Android系统决定重新创建该Activity,系统会将这个Bundle对象传回,并在新创建的Activity中显示Bundle的数据。

IllegalStateException出现的原因和onSaveInstanceState()有很大关系,应用程序的状态只会被该方法的参数中的Bundle对象保存下来,也就是说,如果FragmentTrasaction.commit()方法如果在onSaveInstanceState()之后被调用,那么这个transaction将不会被作为Activity的状态而保存下来。从用户的视角来说,他们会认为这个transaction丢失了,造成了UI的状态丢失。为了提高用户体验,Android系统在在遇到状态丢失这种情况时,会抛出IllegalStateException异常。

异常出现的地点

如果你以前遇到过这种异常,那么可能你会注意到它在不同Android版本发生的地点是不同的。比如,你可能注意到在老的机器中,它发生的几率更低,或者使用support lib时,其发生的几率更大。这种不一致造成了一个观点是support lib存在bug,但事实并非如此。

这种不一致的原因是Android Activity的生命周期在Honeycomb(Android 3.0)的时候发生了变化:
Honeycomb之前,Activity必须在pause之后才能被kill掉,意味着onSaveInstanceState()方法会恰好在onPause()方法之前(紧邻着)被调用;
honeycomb以及之后的版本,Activity需要在stop之后才能被kill掉,所以onSaveInstanceState()会在onStop之前调用。

 Honeycomb之前Honeycomb之后
Activity在onPause()之前被kill
Activity在onStop()之前被kill
onSaveInstanceState()调用地点(紧邻)onPause()之前onStop()之前

因为Android Activity生命周期的更改,support lib会根据不同版本提供不同的功能。比如在Honeycomb之后的版本,当commit()在onSaveInstanceState()之后被调用,则这个异常会被抛出。然而,因为在Honeycomb之前的Android版本中,onStateInstanceState()被调用处在Activity生命周期更早的地方,如果适用于同样的异常抛出规则,则这个异常会被频繁抛出,不利于编程,在权衡了程序编写和用户体验之后,Android做了一个妥协:处在onPause()和onStop()之间的commit()将不会抛出异常,但是会有Activity状态丢失的后果。

 Honeycomb之前Honeycomb之后
commit()在onPause()之前OKOK
commit()在onPause()和onStop()之间状态丢失OK
commit在onStop之后ExceptionException


避免异常的建议

以上介绍了IllegalStateException出现的原因和地点,重要的是理解support lib的工作方式,以及避免状态丢失的重要性。下面是关于如何尽量避免该异常的建议:

1. 需要尽量避免在某个 Activity生命周期的某个方法中进行Transaction的commit()操作。当然首次调用onCreate()方法例外,此时bundle为null。如果应用程序中只在首次onCreate()中调用了Transaction的commit(),可以保证永远不会出现IllegalStateException。然而,如果commit操作是在onActivityResult()、onStart()、或者onResume()中被调用的,就可能会出现问题。比如,如果你在onResume()方法中调用commit。根据Android官方文档:
protected void onResume ()

Dispatch onResume() to fragments. Note that for better inter-operation with older versions of the platform, at the point of this call the fragments attached to the activity are not resumed. This means that in some cases the previous state may still be saved, not allowing fragment transactions that modify the state. To correctly interact with fragments in their proper state, you should instead override onResumeFragments().

FragmentActivity的onResume方法不能保证某个Fragment已经resume,造成了保存的还是原来的状态,所以这里不允许commit的操作。

当你需要Transaction的commit操作的时候,可以使用FragmentActivity的onResumeFragments()或者Activity的onPostResume()方法。Android系统保证了这两个方法会在Activity的状态恢复之后才被调用,也就会避免了可能的状态丢失。

举个例子说明如何使用onPostResume()方法来避免onActivityResult()中进行commit:
这里使用的是一个标志mReturningWithResult,默认为false,当有满足条件的返回时,将其设置为true,不做commit操作,而是将commit操作转入onPostResume()方法中,根据mReturningWithResult决定是否进行commit操作。

private boolean mReturningWithResult = false;

@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    mReturningWithResult = true;}

@Overrideprotected void onPostResume() {
    super.onPostResume();
    if (mReturningWithResult) {
        // Commit your transactions here.
    }
    // Reset the boolean flag back to false for next time.
    mReturningWithResult = false;}
因为Android系统保证了onResume()会在onAcitivtyResult之后被调用,而顾名思义onPostResume()在onResume()之后调用,并且这时候Activity的状态(包含内部Fragment的状态)都已经恢复,所以这时候做的commit操作可以避免状态丢失。

下一篇会继续介绍尽量避免IllegalStateException的思路,比如在AsyncTask中避免IllegalStateException

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值