android.os.TransactionTooLargeException on Nougat
Solution 1:
Whenever you see TransactionTooLargeException
happening when an Activity
is in the process of stopping, that means that the Activity
was trying to send its saved state Bundles
to the system OS for safe keeping for restoration later (after a config change or process death) but that one or more of the Bundles
it sent were too large. There is a maximum limit of about 1MB for all such transactions occurring at once and that limit can be reached even if no single Bundle
exceeds that limit.
The main culprit here is generally saving too much data inside onSaveInstanceState
of either the Activity
or any Fragments
hosted by the Activity
. Typically this happens when saving something particularly large like a Bitmap
but it can also happen when sending large quantities of smaller data, like lists of Parcelable
objects. The Android team has made very clear on numerous occasions that only small amounts of view-related data should be saved in onSavedInstanceState
. However, developers have often saved pages of network data in order to make configuration changes appear as smooth as possible by not having to refetch the same data again. As of Google I/O 2017, the Android team has made clear that the preferred architecture for an Android app saves networking data
- in memory so it can be easily reused across configuration changes
- to disk so that it can be easily restored after process death and app sessions
Their new ViewModel
framework and Room
persistence library are meant to help developers fit this pattern. If your problem is with saving too much data in onSaveInstanceState
, updating to an architecture like this using those tools should fix your problem.
Personally, before updating to that new pattern I'd like to take my existing apps and just get around the TransactionTooLargeException
in the meantime. I wrote a quick library to do just that: https://github.com/livefront/bridge . It uses the same general ideas of restoring state from memory across configuration changes and from disk after process death, rather than sending all that state to the OS via onSaveInstanceState
, but requires very minimal changes to your existing code to use. Any strategy that fits those two goals should help you avoid the exception, though, without sacrificing your ability to save state.
On final note here : the only reason you see this on Nougat+ is that originally if the binder transaction limit was exceeded, the process to send the saved state to the OS would fail silently with only this error showing up in Logcat:
!!! FAILED BINDER TRANSACTION !!!
In Nougat, that silent failure was upgraded to a hard crash. To their credit, this is something the development team documented in the release notes for Nougat:
Many platform APIs have now started checking for large payloads being sent across Binder transactions, and the system now rethrows TransactionTooLargeExceptions as RuntimeExceptions, instead of silently logging or suppressing them. One common example is storing too much data in Activity.onSaveInstanceState(), which causes ActivityThread.StopInfo to throw a RuntimeException when your app targets Android 7.0.
Solution 2:
In the end, my problem was with things that were being saved onSaveInstance, and not with things that were being sent to the next activity. I removed all saves where I can't control the size of objects (network responses), and now it's working.
Update 2:
Google now provides AndroidX ViewModel which is based on the same technology as retained Fragments but much easier to use. Now ViewModel is a preferred approach.
Update 1:
To preserve big chunks of data, Google is suggesting to do it with Fragment that retains instance. Idea is to create an empty Fragment without a view with all necessary fields, that would otherwise be saved in the Bundle. Add setRetainInstance(true);
to Fragment's onCreate method.
And then save data in Fragment on Activity's onDestroy and load them onCreate.
Here is an example of Activity:
public class MyActivity extends Activity {
private DataFragment dataFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}
// the data is available in dataFragment.getData()
...
}
@Override
public void onDestroy() {
super.onDestroy();
// store the data in the fragment
dataFragment.setData(collectMyLoadedData());
}
}
An example of Fragment:
public class DataFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public void setData(MyDataObject data) {
this.data = data;
}
public MyDataObject getData() {
return data;
}
}
More about it, you can read here.
Solution 3:
Did a hit and trial, and finally this solved my issue.
Add this to your Activity
@Override
protected void onSaveInstanceState(Bundle oldInstanceState) {
super.onSaveInstanceState(oldInstanceState);
oldInstanceState.clear();
}
Solution 4:
The TransactionTooLargeException has been plaguing us for about 4 months now, and we've finally resolved the issue!
What was happening was we are using a FragmentStatePagerAdapter in a ViewPager. The user would page through and create 100+ fragments (its a reading application).
Although we manage the fragments properly in destroyItem(), in Androids implementation of FragmentStatePagerAdapter there is a bug, where it kept a reference to the following list:
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
And when the Android's FragmentStatePagerAdapter attempts to save the state, it will call the function
@Override
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i=0; i<mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null && f.isAdded()) {
if (state == null) {
state = new Bundle();
}
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
}
}
return state;
}
As you can see, even if you properly manage the fragments in the FragmentStatePagerAdapter subclass, the base class will still store an Fragment.SavedState for every single fragment ever created. The TransactionTooLargeException would occur when that array was dumped to a parcelableArray and the OS wouldn't like it 100+ items.
Therefore the fix for us was to override the saveState() method and not store anything for "states".
@Override
public Parcelable saveState() {
Bundle bundle = (Bundle) super.saveState();
bundle.putParcelableArray("states", null); // Never maintain any states from the base class, just null it out
return bundle;
}
Solution 5:
I face this issue as well on my Nougat devices. My app uses a fragment with a view pager which contains 4 fragments. I passed some large construction arguments to the 4 fragments which caused the problem.
I traced the size of Bundle
causing this with the help of TooLargeTool.
Finally, I resolved it using putSerializable
on a POJO object which implements Serializable
instead of passing a large raw String
using putString
during fragment initialization. This reduced size of Bundle
by half and does not throw the TransactionTooLargeException
. Therefore, please make sure you do not pass huge size arguments to Fragment
.
P.S. related issue in Google issue tracker: https://issuetracker.google.com/issues/37103380