API11(Honeycomb) 以降で Fragment Transaction#commit() のタイミングが変わる件

 

クラッシュと_ANR_-Play_Developer_Console
Honycomb(API11) 以降で問題のスタックトレース。

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)

 

なぜExceptionが?

Activity が保存された状態でコミットされて、それのステートの損失が確認された結果です。
onSaveInstanceState()が呼び出されたときに何が起きているのかを見てみましょう。


Androidシステムは、メモリを解放するために、いつでもプロセスを終了させる機能をもっています。フレームワークは、各Activityを終了させる前にSaveInstanceState() メソッドを呼び出すことにより、その状態を保存可能な状態としています。保存された状態を後で復元されると、ユーザーは、それらがシームレスに Activity がシステムによって終了されたかどうかにかかわらず、バックグラウンドとの間で切り替えているという認識を与えてくれます。

なぜ、exception が発生するかというと、Fragment の状態が保存されていないからです。
これは、onSaveInstanceState() の後、commit() を呼び出しているということになります。
ユーザの観点からは、トランザクションが偶発的UI状態の損失をもたらす、または、失われたように表示されることになる場合、ユーザーエクスペリエンスを保護するために、Androidはすべてのコストでステートの損失を回避し、それが発生するたびに、IllegalStateException をスローします。

 

いつ Exception が throw されているか?

サポートライブラリにバグがあるのでは、といわれているがそうでもないと思われます。
実は、Honeycomb(API11) 以降ライフサイクルが変わっています。

  Honeycombまで Honeycomb以降
onPause() の前に Acticity が終了される? されない されない
onStop() の前に Activity が終了される? される されない
onSaveInstanceState(Bundle) が何が呼ばれる前まで保証されているか onPause() onStop()

Activity のライフサイクルが変更された結果、サポートライブラリは、プラットフォームのバージョンに応じて動作を変更する必要があります。
プラットフォームの旧バージョンとの互換のために、onPause()/onStop() の動作を考慮しましょう。

  Honeycombまで Honeycomb以降
onPause() の前の commit() OK OK
onPause() と onStop() の間の commit() STATE LOSS OK
onStop() の後の commit() EXCEPTION EXCEPTION

 

Exception を避けるには?

アクティビティのライフサイクルメソッドの内部でトランザクションをコミットするときには注意。

FragmentActivity#onResume() ではなく FragmentActivity#onResumeFragments() or Activity#onPostResume() 内で、commit() でする。

非同期コールバックメソッド内でトランザクションを実行することは避ける。

AsyncTask#onPostExecute() や LoaderManager.LoaderCallbacks#onLoadFinished() などは、Activityライフサイクルの状態を知らない。

よくある例:

1. ActivityがAsyncTaskを実行。
2. ユーザがホームキーを押して、ActivityのonsaveInstanceState()と onStop()が呼ばれる。
3. AsyncTaskがバックグラウンド処理を完了して onPostExecute() が呼ばれるが、Activityが停止してることは認識していない。
4. Fragment Transactionがcommitされ結果として、ステートがロスしてるので exception。

アプリケーションがこれらのコールバックメソッド内でトランザクションを実行する必要があり、コールバックがonSaveInstanceState()の後に呼び出されないことを保証する簡単な方法がない場合は、commitAllowingStateLoss()を使用する。

 

commitAllowingStateLoss() を使う?

commit() とcommitAllowingStateLoss() の違いは、Activity の損失が発生した場合、後者は例外をスローしないということだけです。状態が失われる可能性を回避することができない場合を除き、commitAllowingStateLoss() は使用すべきではないです。

 

2.x系への下位互換やFragment化の移行であたしを含めてよくある問題だと思われます。
当分の間は考慮する必要があると思われます。

Android Support Library のよくあるクラッシュ その1「安全な Fragment Transaction の実行」 Android Support Library のよくあるクラッシュ その1「安全な Fragment Transaction の実行」
FragmentTransactions & Activity State Loss | Android Design Patterns FragmentTransactions & Activity State Loss | Android Design Patterns


関連ワード:  AndroidAndroidStudioライブラリ開発