昨年必要に迫られてtargetSdkVersionを上げた弊社アプリですが、最近こんなエラーがCrashlyticsに届くようになりました。
Caused by android.os.TransactionTooLargeException: data parcel size 542192 bytes
at android.os.BinderProxy.transactNative(BinderProxy.java)
at android.os.BinderProxy.transact(BinderProxy.java:1127)
at android.app.IActivityManager$Stub$Proxy.activityStopped(IActivityManager.java:4027)
at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:144)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:280)
at android.app.ActivityThread.main(ActivityThread.java:6748)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
先人の知恵をお借りすると、どうやらAndroid 7.0のときに onSaveInstanceState
で引き継げるデータ量に制限がかかったようです。
- Android 7(Nougat) におけるonSaveInstanceStateとTransactionTooLargeException - Qiita
- Binder transaction buffer は1MBとは限らないかもしれない話 - nashcft's blog
上記の記事内でも紹介されていますが、公式ドキュメントに次のような記述があります。
多くのプラットフォーム API は、Binder トランザクションで送信される大きなペイロードをチェックし、暗黙的にログ記録したり、削除したりするのではなく TransactionTooLargeExceptions を RuntimeExceptions として再度スローするようになりました。一般的な例としては、Activity.onSaveInstanceState() で大量のデータを格納することです。これにより、アプリが Android 7.0 をターゲットにしている場合は、ActivityThread.StopInfo で RuntimeException がスローされます。
Android 7.0 の動作の変更点 | Android Developers
https://developer.android.com/about/versions/nougat/android-7.0-changes.html?hl=ja#other
弊社の事例でも、実際にそこそこ大きなデータを突っ込んでおり、更にそれが意図しない分量だったことがわかったので、データ量を削減することで対処することができました。
この問題について調べていて、思うところがあったので、書いていきたいと思います。
1MB未満でも起きるの!?
Activity間の画面遷移を行う際に、Intentに載せられるExtraのペイロードの上限が1MBであることは、Extraを過信して濫用したことがある多くのAndroidエンジニア(もちろん私もです)がご存知のとおりです。これはTransactionTooLargeExceptionの公式ドキュメントにも次のように記述されています。
The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all transactions in progress for the process.
onSaveInstanceState
に代表される、Android 7.0での新たなチェック対象も、ドキュメントに従うならば1MBを上限にするはずです。
しかし、実際に私たちの手元で起きた事例は、542192 bytes ≒ 529.4KBという、1MBよりは遥かに小さなものでした(とはいえ大きいよねという話題は後述します)。
デバイスによっては512KB以下でも起きることがあるっぽいなんて話もあるくらいなので、ドキュメントの記述のほうが古いと見たほうがいいのかもしれません。
まあ大きいデータを扱わないほうがいい
512KBはテキストデータの集合だと考えても十分に大きい類なので、エラーが出ること自体は妥当です。
では、私たちはどうしたらいいのかというと、BundleにはIDなどの「あとで復元するときにポインタとして使える情報」だけを残しておいて、大きなデータは素直に永続化領域に保存しておいたり、ネットワークから取り直したりしましょう、というのが、真っ当な選択肢になるかなと思います。
ただ、これは画面回転のときには適用しづらくて、画面回転ごときで再フェッチなんてしていられません。
そこで役に立つのが、Android JetpackのViewModelです。ViewModelは画面回転をまたいで生存することができるので、 onSaveInstanceState
に頼らなくても画面遷移の前後でデータを引き継ぐことができます。
まとめ
onSaveInstanceState
のデータ上限チキンレースをするのも不毛なので、最小限のデータだけを渡せる設計にしておきましょうね。