「译」Fragment事务与Activity状态丢失

原文来自这里
欢迎转载,但请保留译者出处:http://www.jianshu.com/p/3d8d78bf38ee

自从Honeycomb(译者注:Android 3.1)初版发布以来,如下stack trace与异常信息就让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)

本文旨在说明何种原因何种时刻这一异常会被抛出,并且总结出了几种建议用于帮你确保你的应用再也不会因为它而crash。

为什么这异常会被抛出?

这一异常之所以被抛出,是因为你意图在activity的状态被保存后提交一个Fragment事务(FragmentTransaction),于是引发了一种命名为Activity状态丢失Activity state loss)的现象。在我们深入了解这个词汇的实际含义前,不妨先看一下当onSaveInstanceState()被调用时究竟发生了些什么。如我在我的上一篇文章Binders & Death Recipients里说到的那样,Android应用在Android运行时环境中对于它自身的命运只有非常少的控制权。而Android系统拥有在任何时刻结束进程以释放内存的权限,就结果来说,后台activities 可能会被杀死却收不到一丁点儿警告。为了确保这一偶然发生的古怪行为对用户而言不可感知,framework 将给予每一个Activity 一次机会来保存好它的状态,具体来说就是在将Activity 变得易于被销毁之前顺手调用一下它的onSaveInstanceState()方法。这样不管Activity 是否曾经被系统杀死过,当保存的状态在之后被还原,都能让用户在前台后台之间切换activities时有一种无缝的感受。

当framework 调用onSaveInstanceState()时,就会传给这个方法一个Bundle对象,而Activity 可以用这个对象来保存它自己的状态,具体来说Activity 可以在里面保存自己的dialogs, fragments,还有views的状态。当方法返回时,系统会把这个Bundle打包通过一个Binder 接口传到System Server process中去,在那里这个Bundle会被保存得很好。当系统之后决定重新创建这个Activity时,同样的Bundle对象就会被传递给应用,用来让它能够还原Activity被杀死之前的状态。

所以说为什么这异常会被抛出?嗯,这一问题就只是起源于这样一个事实而已:Bundle对象在onSaveInstanceState()被调用后就成为了一个代表Activity 状态的快照。这意味着当你在onSaveInstanceState()被调用后调用FragmentTransaction#commit()的话,那么这一事务将不会被记住——因为它压根儿就没有机会被记录为Activity 状态的一部分了。从用户视角来看待这一问题,这个事务的丢失将导致意外的UI状态丢失。为了保护用户体验,Android 不惜任何代价避免“状态丢失”,所以当这种事发生时就会简单地抛出一个IllegalStateException异常给你。

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

如果你之前已经遇到过这一异常了,那么很可能你已经注意到在不同的平台版本之间,这一异常被抛出的概率有一些不一致。举例来说,你很可能发现在旧设备上这一异常抛出得没有这么频繁,或是你的应用在使用support library时会比使用official framework classes时更有可能发生crash。这些轻微的不一致现象让许多人猜测support library存在bug不可信赖。然而,这种猜测基本上是不正确的。

关于这些轻微的不一致现象存在的原因,则是起源于Honeycomb版本中对Activity 生命周期的一项重大改变。在Honeycomb之前,Activity 不能被作为可杀死的对象直到它已经暂停过后,意味着onSaveInstanceState()会在onPause()调用之前马上调用。而从Honeycomb开始,Activity 只能在它已经停止过后才能被作为可杀死的对象,意味着现在onSaveInstanceState()会在onStop()调用之前调用而不是在onPause()调用之前马上调用。这些不同点被总结在了如下表格中:

pre-Honeycomb post-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 生命周期重大改变的结果,support library 有时需要根据平台版本来改变它的行为。举例来说,在Honeycomb 及其之后的设备上,每一次commit()onSaveInstanceState()之后调用都会抛出一个异常用于警告开发者发生了状态丢失。然而,在pre-Honeycomb的设备上每次发生状态丢失时就抛出异常将会带来过多限制,那些设备在Activity生命周期要早得多的时候就会调用onSaveInstanceState(),并且更可能发生意外状态丢失。Android 团队被迫作出妥协:让旧的平台版本有着更好的inter-operation(“for better inter-operation with older versions of the platform”,译者:这个不会翻了Orz),旧设备不得不与可能发生于onPause()onStop()之间的意外状态丢失共存。support library在两种平台上的不同行为由下表进行了总结:

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

如何避免这一异常?

一旦你理解实际上究竟发生了什么,那么避免Activity 状态丢失就变得整个都容易起来。如果你已经达到了这篇文章里提到的高度,希望你能对于support library如何工作还有为什么避免你的应用发生状态丢失是如此的重要理解得更好。当你在你的应用中使用FragmentTransaction时,万一你是在搜索快速解决方案时参考到这篇文章,这里有几条建议需要你记在脑海之中:

  • 在Activity 生命周期方法中提交事务时保持小心谨慎 大部分应用只会在最一开始的onCreate()方法之中还有(或者)在对用户输入进行反馈的时候提交事务,这样一定不会面临任何问题。而当你的事务开始冒险在 Activity 生命周期的其他方法中提交时,像是onActivityResult(),onStart()onResume()之类,事情将变得有些棘手。举个例子,你不应该在FragmentActivity#onResume()方法中提交事务,因为这个方法存在几种当Activity 状态还没有被还原时就被调用的情况 (see the documentation for more information)。如果你的应用需要在Activity 生命周期方法中(非onCreate())提交事务,要么在FragmentActivity#onResumeFragments()中,要么选择Activity#onPostResume()。这两个方法能保证是在Activity还原至原先状态后才被调用,因此都能避免状态丢失的可能性(As an example of how this can be done, check out my answer to this StackOverflow question for some ideas on how to commit FragmentTransactions in response to calls made to the Activity#onActivityResult() method)。
  • 避免在异步回调方法中使用事务 这一点包括通常使用的方法像是AsyncTask#onPostExecute()LoaderManager.LoaderCallbacks#onLoadFinished()。在这些方法中使用事务的问题在于,当它们被调用的时候它们并不具备知晓当前Activity 生命周期状态的认知力。举个例子,考虑下面这个事件序列:

    1. 一个activity 启动了一个AsyncTask
    2. 用户按下"Home",这将令activity 的onSaveInstanceState()还有onStop()方法被调用
    3. 那个AsyncTask这时完成了,于是其onPostExecute()方法在不知晓activity 已经停止的情况下被调用了
    4. 因为一个FragmentTransaction在onPostExecute()中被提交,造成了异常被抛出

    总的来说,在这样的例子中避免异常的最佳方法无过于简单地避免在异步回调方法中提交事务。Google工程师似乎也很赞同这一信条。根据这篇Android Developers group的文章,Android 团队表示能够在异步回调方法中提交FragmentTransaction造成UI变换将会是糟糕的用户体验。如果你的应用需要在这些回调方法中提交事务,那么并没有简单的方法能够确保这些回调不会是在onSaveInstanceState()之后才被调用,你也许不得不求助于使用commitAllowingStateLoss(),同时还要处理可能发生的状态丢失(See also these two StackOverflow posts for additional hints, here and here)。

  • 使用 commitAllowingStateLoss() 仅作为最后手段 调用commit()commitAllowingStateLoss()之间的惟一区别仅在于后者不会抛出异常,即使发生了状态丢失。通常你不会想要使用这一方法,因为这暗示了你的应用中存在发生状态丢失的可能性。更好的方法当然是写好你的应用,让commit()总是在activity 状态被保存之前调用,这将会达到更佳的用户体验。除非状态丢失存在着无可避免的可能性,要么就不应该使用commitAllowingStateLoss()

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!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容