---
title: "TransactionTooLargeException and a Bridge to Safety -- Part 2"
source: "/writing/transactiontoolargeexception-and-a-bridge-to-safety-part-2/"
---

Article

# TransactionTooLargeException and a Bridge to Safety -- Part 2

Brian Yencho

December 18, 2019

Photo by [Immortal shots](https://www.pexels.com/photo/aerial-view-of-bridge-1211856/)

In the [first article of this series](/writing/transactiontoolargeexception-and-a-bridge-to-safety-part-1) , I discussed what `TransactionTooLargeException` is, when and why it happens, and how the `[Bridge](https://github.com/livefront/bridge)` library can be used to avoid the problem. If you are not yet familiar with how to use `Bridge` you should definitely check out that article before continuing. What I'd like to do now is to take a peek inside the library itself and focus on a few tricks used to make it all work. First, though, we have a bit of bookkeeping to discuss.

A quick look at the documentation will show that all the interactions with `Bridge` take the form of static method calls: `Bridge.saveInstanceState`, `Bridge.restoreInstanceState`, etc. However, there is very little actual code in the `Bridge` class itself. Apart from checks to make sure the `initialize` method has been called, each static method simply calls through to a static instance of a different class, `BridgeDelegate`:

The `Bridge` class is just a wrapper for a static instance of BridgeDelegate.

`BridgeDelegate` is where all the action actually takes places and this will be the focus of the discussion below. It is also important to emphasize that I'll be presenting a somewhat simplified version of that code in order to focus on the core functionality.

## A Look Inside

In order to avoid a `TransactionTooLargeException` when sending too much data across processes after `onSaveInstanceState`, Bridge must do a few key things:

-   Bypass sending the data to `system_process` and store it "locally" (i.e. solely within reach of your app's own process).
-   Save the data both in memory (for quick use across configuration changes) and to disk (for use after process death and recreation).
-   Effectively link a given instance of a class to its own stored data (even after process death and restoration).

Let's take a high-level look at the two main methods in `BridgeDelegate` involved in actually saving and restoring data and then discuss all the key points below:

The primary exposed methods of BridgeDelegate for saving and restoring data.

### Bridge generates keys that are stored in the usual Bundle

The first action taken when attempting to save data is to create a unique key that can be used to find that data later. This is done using a UUID returned from a call to `getOrGenerateUuid`:

A UUID is either generated or retrieved from an internal map.

You'll note that once we have generated a UUID, we will associate that key with the `Object` itself in a `Map`; this allows us to avoid generating multiple keys for the same object, which prevents the same data from being unnecessarily written to disk multiple times later on. It is also very important that the `Map` used is a `WeakHashMap`: because a single instance of `BridgeDelegate` is held as a static variable in the `Bridge` class, holding strong references to the targets themselves would result in memory leaks (and of `Activity` and `Fragment` instances no less!).

Once we have a UUID for the target, that value is then stored in the original `Bundle` sent in with the target:

state.putString(getKeyForUuid(target), uuid);

This UUID is **_the only piece of data_** Bridge will store in the original `Bundle` for a given target and this fact is what prevents `TransactionTooException` from ever occurring: rather than placing an arbitrary amount of data in a `Bundle` we are now only storing a single `String`. In this way, we leverage Android's existing saved state framework to hold the unique locator we will need for "locally" managing the actual saved state ourselves.

Without Bridge, generic Bundle data will be sent directly to system_process via IPC, where it will be subject to a potential TransactionTooLargeException.

With Bridge, only a UUID will be sent to system_process for a given target. The generic Bundle data will be intercepted and stored safely within reach of an application's own process.

Before we move on to the discussion of where the _actual_ saved data goes, let's mention one final point. Note from above that the key used is based on the name of the target class:

When storing the UUID in the original Bundle, the keys used are based on the name of the target class.

This allows Bridge to manage the state of other objects (like presenters or viewmodels) in addition to `Activity` and `Fragment` instances if necessary. Multiple keys can then be safely tracked in the same `Bundle` of a given `Activity` / `Fragment` (assuming, of course, that an attempt is not made to save the state of different instances of the same class in the same `Bundle`, which would not typically be done anyway with or without `Bridge`).

Now let's take a look at where the actual data goes and how the UUID is used to retrieve it.

### Bridge places the actual data in a completely separate Bundle

Next, using the `SavedStateHandler` instance passed to the `Bridge.initialize` call, `Bridge` populates a completely new `Bundle` with the data a caller is actually interested in saving:

Bundle bundle = new Bundle(); mSavedStateHandler.saveInstanceState(target, bundle);

Recall that this `SavedStateHandler` is just an abstraction around libraries like `[Icepick](https://github.com/frankiesardo/icepick)` , `[StateSaver](https://github.com/evernote/android-state)` , etc. that pull data from the annotated fields / properties of the `target` object and place them in the supplied `Bundle`. The usage here follows their typical behavior, but rather than passing the original `Bundle` supplied by the OS in `onSaveInstanceState` we have supplied our own.

### Bridge saves these new Bundles to memory

Now that we have a `Bundle` of our own and a UUID we can use to uniquely match it to the source object, we can begin storing that `Bundle` for later reuse.

Data is saved to both memory and disk.

The first thing we'll do is save it to memory. This is the easy part: we'll simply associate each UUID and `Bundle` pair in a `Map`:

private Map<String, Bundle> mUuidBundleMap = new HashMap<>();

Now, before discussing how we further persist this data to disk, let's review the steps we can use to pull this saved data from memory and restore it to our target object. When we call `restoreInstanceState,` retrieval of this in-memory `Bundle` is simple:

-   As a complement to the `getOrGenerateUuid` method discussed above for getting a new or cached UUID for a given target, we can call `getSavedUuid` to pull the UUID out of the external, OS-provided `Bundle` (or directly from the in-memory map where available).

The UUID for a given target may be cached in memory or saved to disk.

-   If we find an associated UUID, we then call `getSavedBundle` to pull the matching `Bundle` from the in-memory `mUuidBundleMap`, only falling back to the disk-persisted data if it is not already present here.

The Bundle for a given target may be cached in memory or saved to disk.

-   Finally, if we successfully retrieve a `Bundle` we can restore the saved state in question using our `SaveStateHandler` once again:

mSavedStateHandler.restoreInstanceState(target, bundle);

We have now saved and restored a target's data without ever having to send it to `system_process` for safe keeping! Let's now look at the last few tricks used to do the final, key piece of the puzzle: persisting the data to disk.

### Bridge can persist generic Bundles to disk

When it comes to persisting data to disk in Java, the primary means of achieving this generically is via the `Serializable` interface. A quick look at the `Bundle` class -- the form in which all our data is now stored -- reveals, however, that **it does not implement Serializable**. That is pretty bad news for a quick, easy solution. A `Bundle` _is_ `Parcelable`, but that is a format meant for transferring data across processes via a `Parcel`, not for writing it to disk. A `Bundle` also does not exclusively contain simple data types that are _themselves_ `Serializable` or otherwise easily-persistable: in addition to simple types like `String` and `Float` there can also be `Parcelable` data, `IBinder` references, and even other `Bundle` instances!

Fortunately, with a few good assumptions and a lucky bit of documentation, we can in fact find a way to make this work. Let's go back to the earlier statement that the `Bundle` class implements `Parcelable`. Is there anything there that can help us? Well the `Parcelable` interface itself really only allows us to do two things: read from and write to `Parcel` objects.

So what about the `Parcel` class? This is where we start to find our way forward: there is a method called `Parcel.marshall()` that returns the content of the `Parcel` as a `byte` array (and a corresponding `Parcel.unmarshall(byte[], int, int)` to read those same bytes back to a `Parcel`). This is now beginning to look very much like `Serializable`: generic objects go in and bytes come out. If we can use this to get bytes from a generic `Bundle` we should be able to write that `Bundle` to disk however we choose.

Under certain conditions, a Bundle may be safely written out as bytes and then back again.

Before proceeding, let's first take a look at the [documentation](https://developer.android.com/reference/android/os/Parcel.html#marshall\(\)) for `Parcel.marshal()`:

The documentation for the Parcel.marshall() method

OK, so that first sentence is pretty bad news:

> The data you retrieve here **must not** be placed in any kind of persistent storage (on local disk, across a network, etc).

That would seem to put an end to this particular strategy of attempting to save a generic `Bundle` to disk. What ends up saving us, though, is the last sentence:

> The Parcel marshalled representation is highly optimized for local IPC, and as such does not attempt to maintain compatibility with data created in different versions of the platform.

This is the "lucky bit of documentation" I previously mentioned. They could have just left it at "don't do this," but this additional explanation for _why_ you shouldn't attempt to persist these bytes to disk is _just_ enough information to allow us to brazenly disregard the warning and do just that. The reason we can do so brings us to our assumptions:

-   Even though `Bridge` will persist these bytes to disk (in direct violation of the warning) they will only ever be used in process-death-and-restoration scenarios. This means that any fresh launch of an app using `Bridge` will not read this data from disk, let alone pass it to `Parcel.unmarshall(byte[], int, int)`. In particular, updates to the Android version of a user's device will require a device restart, which means that the next time an app using `Bridge` is launched, there will be no associated saved state and`Bridge` will therefore not attempt to read this data in this scenario either. So even though we are persisting the data, if we can assume the documentation's explanation for the warning is both _accurate_ and _complete_ then we know we will not encounter any issues because we will never try to read that data back to an incompatible`Parcel` implementation.
-   The next assumption brings us to something the `Parcel.marshall()` documentation leaves out: that this method will actually crash if it contains references to objects that can not be written out as bytes. This includes obscure things like `[IBinder](https://developer.android.com/reference/android/os/IBinder)` references, some rendering-specific things like `[Surface](https://developer.android.com/reference/android/view/Surface)` , and some more common but very-highly optimized classes like `[Bitmap](https://developer.android.com/reference/android/graphics/Bitmap)` (which delegate their `writeToParcel` calls to special native code). `Bridge` makes the assumption that these kinds of classes are simply not the kind of data that would be _manually_ placed in a`Bundle` in `onSaveInstanceState` (although `Bridge` does actually make an exception for `Bitmap` by "un-optimizing" it when writing to a `Bundle` because hey, you never know).

With these two key assumptions, we can take our generic `Bundle` data, write it to disk, and then read it back later if required for process-death-and-restoration scenarios.

Let's now take a quick look at what that looks like in our code:

The process used by Bridge to write a generic Bundle to disk.

After getting our `byte[]` from the `Parcel`, we Base64 encode them as a `String`. This allows for quick and easy storage in a private `SharedPreferences` instance. Reading the data back is simply the inverse process:

The process used by Bridge to read a Bundle back from disk.

We've gone from `Bundle` to disk and back again!

### Bridge leverages SharedPreferences

One final word on the use of `SharedPreferences`: one usually expects `SharedPreferences` to be used for _small_ amounts of data, not entire objects written to bytes and converted to a `String`. This is typically very important due to the synchronous nature of accessing data stored via `SharedPreferences`. It is worth noting, however, that a given `SharedPreferences` is really just a fancy wrapper around an XML file containing simple key-value pairs and that this wrapper has a number of nice properties for our particular use case:

-   `SharedPreferences` only ever reads its disk content a single time: as soon as a particular instance is retrieved it loads its contents on a background thread and places it into a `Map` in memory. Calls to retrieve data from `SharedPreferences` only block if the data is attempted to be accessed while this background loading is underway; otherwise the data can be accessed immediately from the `Map`. If `Bridge.initialize` is called as one of the very first steps in a custom `Application.onCreate`, it is then possible that the stored data is already in memory by the time it is attempted to be retrieved in the first `Activity` of an application. And if there is still some amount of time the application needs to block while the `SharedPreferences` is loading, this takes place entirely at the startup of the application where small delays are already expected (rather than when navigating between screens, which could otherwise be jarring).
-   Different `SharedPreferences` instances point to different files, which allows all the `Bridge` data to be completely isolated from an application-specific data developers may store in other `SharedPreferences` instances.
-   `SharedPreferences` completely abstracts file handling, which allows the `Bridge` library to focus on the content it is trying to store and less on the details of the disk implementation itself.
-   When using `SharedPreferences.Editor.apply()`, changes can be easily written to disk on a background thread while also guaranteeing that an application won't move to its next lifecycle state before this process is complete. This is very useful for `Bridge`, which must ensure that saved state data is completely written to disk before the application is fully in the "stopped" state when going into the background. Failure to do so might result in the application being killed before all the necessary data has been written to disk.

The complete process from Bundle to SharedPreferences involves Base64 encoding the bytes coming from Parcel.marshall.

## Final Thoughts

We've now reviewed the primary functionality of `Bridge`: saving and restoring arbitrary `Bundle` data without triggering `TransactionTooLargeException`. While `TransactionTooLargeException` may be a seemingly small and isolated problem, I hope to have impressed upon you that it requires more than just a little trickery and technical machinery to beat it.

_Brian works at_ [_Livefront_](https://livefront.com/) _, where it's Bundles all the way down..._

-   Brian Yencho

    Director, Engineering

    [More from Brian](/team/brian-yencho/)

    Brian Yencho

### Tags

[Digital Engineering](/insights/all/?topic=digital-engineering) [Mobile Apps](/insights/all/?topic=mobile-apps)

### Share

-   Copied

## View these next.

[See all](/insights/all/?type=writing)

-   [

    Article

    How To Sabotage Your Project Using Inconsistency

    Collin Flynn

    ](/writing/how-to-sabotage-your-project-using-inconsistency/)
-   [

    Article

    Unit Testing race conditions by creating chaos (Swift)

    Sean Berry

    ](/writing/unit-testing-race-conditions-by-creating-chaos-swift/)
-   [

    Article

    UIApplicationDelegate call sequence reference

    Sean Berry

    ](/writing/uiapplicationdelegate-call-sequence-reference/)
