Fragment Transactions & Activity State Loss

Posted by 彩笔学长 on October 9, 2016

一个异常堆栈

下面所示的异常堆栈追踪在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)`

这篇文章就是来解释这个异常发生的原因和异常抛出的时机,并且给出了一些有益的建议来避免这个异常的发生

为什么会抛出这个异常?

这个异常的抛出是由于你准备在actvity的状态已经被保存后来做一次FragmentTransaction的commit,这将会导致Actvity state loss的现象的出现。

在我们深入讨论这个现象之前,我们先来看一下在 onSaveInstanceState()方法调用后发生了什么。正如我在上一篇博文中提到的http://www.androiddesignpatterns.com/2013/08/binders-death-recipients.html,在android runtime 期间 android应用自己几乎不能控制自己,android系统有权在任何时候释放内存,因此后台的actvity也会被毫无征兆的kill掉。为了确保这种无法估计的行为对用户是无感知的,framework给予每个activity在觉察自己可能(back键除外)会被销毁时调用onSaveInstanceState()来保存自己的状态,用户在前后台切换actvity时,保存的状态数据将会被恢复时,而不会觉察到这个activity是否已经是被系统kill的,在用户看来这是“无缝”切换的。 当framework调用onSaveInstanceState(),它传递一个Bundle对象给actvity来保存它的diaglog、fragments、view的信息 。当方法回调时,系统通过Binder接口传递这个Bundle对象到System Sever,在这里Bundle对象将被安全的存储。然后系统之后重建actvity时再将刚才的Bundle传递给应用,这时actvity得到之前保存的状态

铺垫只是解释完之后下面将详细解释为什么会抛出这个异常。这个问题源于这样一个事实,传递的Bundle对象代表着activity在调用onSaveInstanceState()这一时刻的肖像刻画,这就意味着,在onSaveInstanceState()之后调用FragmentTransaction#commit() 这个transaction将不会被记录,因为它一开始就没有被记录在Activity的状态中。从用户看来,actvity切换恢复时这个transaction体现为丢失的,这将导致Activity的UI状态丢失。为了保护用户的体验,Android不惜一切代价避免状态丢失,出现时就抛出一个异常来提醒开发者。

何时抛出这个异常

如果你之前遇到过这个异常,你可能会注意到,这个异常的抛出随着平台不同的而变得有点不一致。举例来说,你可能会发现老旧的机器抛出这个异常更少些,或者使用support library比官方的framework更容易出现这个异常。这些轻微的矛盾导致了一些诸如“support library 有bug,不可信”的论调,然而这通常都不是真的。

这些轻微矛盾的出现是源于在Honeycomb中Activity生命周期的变化,在Honeycomb之前,actvity在调用OnPause方法之后才能被killed的,这就意味着onSaveInstanceState()需要在在OnPause之前调用。而在Honeycomb版本时,actvity只有在onStop之后才能被killed,这也就意味着onSaveInstanceState()需要在在OnStop之前调用而不是以前版本中的OnPause之前调用

Activity生命周期的微小改变,将使得support library有时需要基于平台来改变它的一些行为,举例来说,在Honeycomb及其以上的设备中,每次在onSaveInstanceState()之后commit都会抛出这个异常,然而在Honeycomb之前的设备中异常就会出现少一些。android团队被迫做出让步:为了老版本的内在更好地兼容,老的设备不得不忍受在 onPause() and onStop()之间的状态丢失support library的行为在不同平台之间总结如下

如何避免这个异常

当你明白到底真正发生了什么你就会发现activity避免状态丢失是多么简单。如果你在这篇博文中做到这一步,希望你能明白整个 support library是如何工作的,并且为什么避免状态丢失是如此重要。为了方便你在这篇博文寻找一个快速修复的方法,这里有一些建议需要牢牢记住,当你在应用中使用 FragmentTransactions的时候

  • 在Actvity的生命周期中commit transactions需要小心谨慎

大多数应用只会在在onCreate中第一次commit transaction,这当然不会遇到这种问题,然而当你的transaction企图在Activity生命周期其他方法,诸如onActivityResult(), onStart(), and onResume()中commit时,这时事情就变得棘手了。举例来说,你不该在 FragmentActivity#onResume()中commit transaction,这是由于有一些情况,这个方法有时候会在activity状态恢复前调用(参考developer.android.com/reference/android/support/v4/app/FragmentActivity.html#onResume())。因此如果你的应用需要在Activity生命周期onCreate()之外commit transaction,只在FragmentActivity#onResumeFragments() orActivity#onPostResume()方法中去commit,这两个方法能够保证Activty状态保存之后再调用,这样就避免了状态丢失(参考http://stackoverflow.com/questions/16265733/failure-delivering-result-onactivityforresult)

  • 避免在异步中执行 commit transactions

这包括了常用的一些方法,诸如:AsyncTask#onPostExecute() and LoaderManager.LoaderCallbacks#onLoadFinished()等。由于这些方法根本不知道在当前Activity哪一个生命周期的去调用了他们,因而在其中commit transaction就会出问题。比如 >An activity executes an AsyncTask. > The user presses the “Home” key, causing the activity’s onSaveInstanceState() and onStop() methods to be called.

The AsyncTask completes and onPostExecute() is called, unaware that the Activity has since been stopped.

A FragmentTransaction is committed inside the onPostExecute() method, causing an exception to be thrown.

总而言之就是要在异步回调方法中避免去commit transaction来避免这个异常的发生。谷歌工程师看起来也是同意这种观念的。通过android gruop的这个文章(https://groups.google.com/d/msg/android-developers/dXZZjhRjkMk/QybqCW5ukDwJ) ,谷歌android team 认为通过异步commit transaction 带来的UI变化并不利于用户体验。如果你的应用不得不在异步中提交,你将不得不使用commitAllowingStateLoss() 并且处理好状态可能丢失的发生的情形(http://stackoverflow.com/questions/7992496/how-to-handle-asynctask-onpostexecute-when-paused-to-avoid-illegalstateexceptionhttp://stackoverflow.com/questions/8040280/how-to-handle-handler-messages-when-activity-fragment-is-paused

  • 将 commitAllowingStateLoss()作为最后使用的手段

commit和commitAllowingStateLoss唯一的区别就是后者在状态丢失时候不会抛出异常,通常不会使用这个方法,因为这将意味着状态的可能丢失。最好的方式就是写好你的应用,这样transaction确保在activity状态保存之后commit,这样用户体验会更好。除非状态丢失不可避免了,否则不要使用commitAllowingStateLoss()