Article
TransactionTooLargeException and a Bridge to Safety — Part 1
February 27, 2019
TL;DR : Stop worrying about TransactionTooLargeException
; use Bridge .
I still remember when I first read the release notes for Android Nougat in the summer of 2016. Buried alongside mostly innocuous changes to things you probably wouldn’t care about was something that jumped out at me:
Many platform APIs have now started checking for large payloads being sent across
Binder
transactions, and the system now rethrowsTransactionTooLargeExceptions
asRuntimeExceptions
, instead of silently logging or suppressing them. One common example is storing too much data inActivity.onSaveInstanceState()
, which causesActivityThread.StopInfo
to throw aRuntimeException
when your app targets Android 7.0.
I would guess that many people glossed over this one, too, and I might have as well if it weren’t for the fact that I’d seen this before. Long ago, I’d seen direct evidence of the OS “silently logging” this problem: I’d noticed some strange state saving behavior and there, buried in all the other logs of the app I was working on, was this singular, aggressively punctuated line:
!!! FAILED BINDER TRANSACTION !!!
And that was it.
After a little investigation, I understood that we were sending too much data in our saved state Bundles. The Bundle couldn’t be sent to the OS for safe keeping, so it was just getting dropped on the floor. For actual users experiencing this situation, the result would be that when the app process was killed while in the background and they returned to it, it would launch from a fresh state rather than a restored one. Less than ideal, sure. But, at the time, it seemed like a graceful enough fallback for the particular edge case I was seeing that “fixing” it became a problem for the backlog.
But then, Nougat happened. I suspected TransactionTooLargeException
would sneak up on a lot of developers, and those suspicions were confirmed when I started seeing all the Stack Overflow questions and “bugs” filed with the Android Issue Tracker, like “TransactionTooLargeException on pausing app” . The message from Google, though, was clear:
Status
Won’t Fix (Intended behavior)
And that’s when I decided to do something about it.
The Problem
Before talking about our solution, let’s first go a little deeper into the actual problem. At its core, Android is just a customized version of Linux in which each application runs in its own separate process. This is true of the core functionality of the operating system itself, which runs in a process very creatively named system_process. In order for an application to function, it needs to communicate with system_process via a special form of inter-process communication (IPC) called “Binder IPC”.
The Binder framework is a fascinating topic that gets to the core of how Android works (and has a history stretching back to Palm OS) but I’ll summarize a few relevant details here:
- Binder IPC allows communication to occur synchronously in each process via a “transact” method. These “Binder transactions” pass data between the processes via highly optimized data containers called Parcel .
- Several familiar Android objects like Intent, Bundle, and Parcelable are ultimately packaged in Parcel objects in order to communicate with system_process.
- Creating / starting / stopping / pausing / resuming / etc. an Android Activity all involve making Binder transactions.
- Each app process has a 1 MB buffer for all Binder transactions.
That last key point is critical : if at any point one of the Parcels becomes so large that its corresponding transaction overflows the 1 MB buffer, we say that the transaction was too large. Hence we get the name, TransactionTooLargeException.
To make matters worse, because the 1 MB limit is on the *buffer* and not on an individual *transaction*, the transactions that actually push things over the limit are typically much smaller and more like ~0.5 MB. Unless you’re simply storing a few primitives here and there, that’s not a lot of data.
This brings us to one of the key places this can all go wrong in an app : onSaveInstanceState
. An example implementation might look something like the following:
This method is called in an Activity when it is in the process of being placed in the “stopped” state. When that happens, the OS needs to acquire all the relevant information for that Activity that it might need to later “restore” it, whether that’s after a configuration change or when the app’s process is killed while backgrounded and the user returns to it. The data collected in the Bundle
passed to this method is then converted to a Parcel
and sent directly to system_process via a Binder transaction. If the custom mData
object is so large that the transaction fails, you’ll see something like the following error message:
That’s the TransactionTooLargeException
we want to solve.
A Word of Caution
Before continuing, let us first state the obvious : the best way to avoid TransactionTooLargeException
is to avoid being in the position to trigger it in the first place. That means following the recommendations that have always been given by Google : just don’t put too much stuff in the saved state Bundle. For a “modern” description of Google’s recommended way of handling state saving and restoration, there’s a great article by Lyla Fujiwara called “ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders” that discusses how to best use the onSaveInstanceState
callback in conjunction with ViewModel and Room from the Android Architecture components. To summarize the key, general points:
- Just put simple data like identifiers in the
onSaveInstanceState
Bundle. - For state that you only need to save across configuration changes (like rotations), just save it in memory.
- For any state you really want to have after the app is restored after process death, you should persist it to disk in some kind of database.
These are all great ideas and should absolutely be followed, but there may be some cases where that it is not immediately possible or preferable: you may not have the time or budget for a large refactor; you might only have this problem in one isolated part of your app and under very unique conditions; maybe you just have an architectural pattern you really like that makes heavy use of the onSaveInstanceState
method.
This is why we created Bridge , a simple library for avoiding this problem with minimal code changes to an existing app. In addition to hinting at its role in helping transport data from one location to another, its name is also meant to imply that this might not be the ultimate solution to your problem, but just something that helps you get there. Buying time to stop and think can be a powerful thing.
Bridge and How To Use It
Let’s finally take a look at an example of using Bridge. First, here’s what an Activity might look like before using it:
Here we are using the excellent Icepick library to manage the saved state of the custom Parcelable
data class, DataModel
, using an @State
annotation. As previously described, it is the saving of this model that might become too large under certain conditions and trigger a TransactionTooLargeException
.
Bridge is actually modeled after Icepick and its usage is meant to be as close to a drop-in replacement as possible. Simply replace the Icepick methods with the corresponding Bridge ones and optionally add a call to Bridge.clear()
in onDestroy
:
As one final step, all you need to do is initialize the library in the Application.onCreate
of you app:
The SavedStateHandler
allows you to choose the library (or custom code) that will actually do the grunt work of reading and restoring saved state for your objects while the Bridge library handles the storage of that data in a safe way.
Like Icepick, Bridge works best when combined with a base class for your Activities / Fragments / etc:
This allows your classes to safely save state without repeating the same boilerplate over and over:
And that’s all it takes to to go from an app suffering from TransactionTooLargeException
to one that doesn’t!
So now you’ve seen what TransactionTooLargeException
is, why we built Bridge to avoid it, and how to use it. To see what Bridge is actually doing under-the-hood, check out Part 2 .