RIP TransactionTooLargeException
A normal sprint release and again the same exception on your crashlytics dashboard. The famous and deadly TransactionTooLargeException occurring on Android devices running Nougat. I personally feel that it is the developer’s responsibility not only to release new features but also to ensure that the application becomes as stable as possible by fixing crashes thus affecting a small (<0.5%) number of users. As google states:
TransactionTooLargeException occurs because the Parcel objects stored in Binder transaction buffer exceeds the limited size of 1Mb. The Binder transaction buffer is shared by all transactions in progress for the process. Consequently this exception can be thrown when there are many transactions in progress even when most of the individual transactions are of moderate size.
So why this exception is only happening on Nougat devices and why not in older versions. Seems like, android used to give internal warning only about the large size of saved state and nothing was crashed on android OS versions lesser than 7. The hunt to fix this crash had already begun and in that way I went through a series of articles mainly:
- https://issuetracker.google.com/issues/37103380
- http://blog.sqisland.com/2016/09/transactiontoolargeexception-crashes-nougat.html
- https://developer.android.com/reference/android/os/TransactionTooLargeException.html
- http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception
This is what Google states to fix this crash:
The key to avoiding TransactionTooLargeException
is to keep all transactions relatively small. Try to minimise the amount of memory needed to create a Parcel
for the arguments and the return value of the remote procedure call. Avoid transferring huge arrays of strings or large bitmaps. If possible, try to break up big requests into smaller pieces.
Hmmm, good enough information to fix the crash, right? Well let me explore the possibilities and situations which Google recommends me to do. So I went through our complete code base (To be noted that we at 1mg follow clean code architecture by Uncle Bob with MVP and RxJava at the core) to look out for those activities or fragments where we are storing a bigger data probably bitmap or huge array of strings. But I could not find any such instance and the crash was still laughing at me(evil grin) saying Catch Me If You Can.
So the information by google could not suffice my thrust to fix the crash. What other options I had. Well I looked out to a stack overflow post and properly analysed every answer of it. An answer by user IK828 stated as to how he fixed the same crash.
Now let me tell you the cause of this crash in our codebase now. We are using FragmentStatePagerAdapter in ViewPager to show banners on our home page which will loop through infinitely and automatically i.e. the next banner(page) will be auto changed on certain time interval and when you have reached the end, the page will loop to start from the beginning again. We are using LoopingPagerIndicator (Thanks to Ali Muzaffar) to achieve the above goal which tweaks the infinite looping by providing the count of pages to Integer.MAX_VALUE. But this tweak leads to the problem of saving the states of large number(>100) of fragments by FragmentStatePagerAdapter.
FragmentStatePagerAdapter keeps a reference to the following list:
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
When FragmentStatePagerAdapter attempts to save the data, it calls the following 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 IK828 also stated, even if we were properly managing 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. I could not do anything for the native Android code but what I can do is override the saveState() implementation in my BannersPagerAdapter(which extends FragmentStatePagerAdapter) and not allow anything to be stored for array “states”.
@Override
public Parcelable saveState()
{
Bundle bundle = (Bundle) super.saveState();
if (bundle != null)
{
// Never maintain any states from the base class, just null it out
bundle.putParcelableArray("states", null);
} else
{
// do nothing
}
return bundle;
}
So I made one more attempt and hoped that it will fix the crash in our next release. We recently released our 1mg app with this fix along with other awesome features. I patiently waited, and today when I opened crashlytics dashboard to see if the crash was fixed or not, I was in 7th heaven to find that the crash has been perfectly fixed for all users.
I can recall the quote by Sidney Markowitz:
The last bug isn’t fixed until the last user is dead.
As a developer who is trying very hard each and every day to make the health care app of 1mg more awesome and user friendly, the feat may be small, was no less than a celebration. Kudos to everyone who helped me in knowing more about this exception and causes. So if you find any unusual bug, just own it and give your best to fix it.
You can download our 1mg app from the play store. Stay healthy.