Demystify TransactionTooLargeException

Rex Huang
ShopBack Tech Blog

--

Recently, ShopBack Android app had encountered a stability issue caused by TransactionTooLargeException which led app crash-free rate drop significantly. Fortunately, we know that the exception was thrown when app shown store page then the user backgrounded the app. Here is the call stacks when the exception occurs:

Caused by android.os.TransactionTooLargeException
data parcel size 1370268 bytes

android.os.BinderProxy.transactNative (BinderProxy.java)
android.os.BinderProxy.transact (BinderProxy.java:766)
android.app.IActivityManager$Stub$Proxy.activityStopped (IActivityManager.java:4867)
android.app.ActivityThread$StopInfo.run (ActivityThread.java:4238)
android.os.Handler.handleCallback (Handler.java:790)
android.os.Handler.dispatchMessage (Handler.java:99)
android.os.Looper.loop (Looper.java:210)
android.app.ActivityThread.main (ActivityThread.java:7080)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:523)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:863)

According to the call stacks, it only shows that exception was thrown by Android framework and didn’t include any of our application code. So it is not easy to find out the reason in intuitive ways. Here I would like to share how I identified the root cause and the lesson learned from this issue.

Analysis

First of all, the exception is thrown when app goes to background during binder transaction, and the reason for this exception is that the buffer size of each remote procedure call(IPC) is limited to 1 MB, if the buffer size is over 1 MB, the exception will be thrown. Although it is easy to know how the exception occurs, we still don’t know which data with a large size causes app to crash.

According to the call stacks, we can suspect this issue might be related to onSaveInstanceState() which gives Activity/Fragment a way to save UI states to Android system before app goes to background in order to restore app’s UI state in case the app’s process is killed by Android OS. However, after checking the app’s implementation, we didn’t even implement activity’s and fragment’s onSaveInstanceState() callback. So we need to dive into framework code to check what data will be stored silently when onSaveInstanceState() is called.

Source: http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/ActivityThread.java#4190

Above code snippet is the starting point of activity’s onStop() handling flow in Android framework. ActivityClientRecord is the data structure used to keep activity’s instance and some information related to the activity by framework. The function does three things below:

  1. Get the correspondingActivityClientRecord instance of the target activity which is going to Stop state.
  2. Pass above ActivityClientRecord instance to performStopActivityInner() that we will go into this method for details later.
  3. Set aboveActivityClientRecord instance to StopInfo, and use StopInfo to notify Android system the activity is no longer visible to the user.

We notice that it sets ActivityClientRecord’s state field which is a Bundle object to the StopInfo (ie. stopInfo.setState(r.state)), so let’s keep tracing the source code to check what does StopInfo do.

Source: http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/servertransaction/PendingTransactionActions.java#139

In the run() method ofStopInfo, the Bundle object(mState) will be send to Activity Manager Service(AMS) by binder transaction and if its size is too big, the exception will be thrown when the Android version is greater than 6. This symptom also matches the Firebase Crashlytics result that almost all crashes occurred in Android 7 and above.

Furthermore, you may notice that framework has put development log for developers to check bundle size if there is an exception occurs (so sweet 😍). We can use adb command to check whether the size of Bundle object exceeds limit or not as following screenshot. Obviously, the size was too big when the exception occurred.

Now we know the Activity where the exception occurs truly keeps too many data when our app goes to background, Besides, because of the key word in log, android:support:fragments, we can suspect that the data may be kept by the fragments in the Activity, but we still don’t know what’s the data it keeps, so let’s keep tracing the source code.

Source: http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/ActivityThread.java#4132

Continue from performStopActivityInner() mentioned before, it invokes above function, callActivityOnStop() invokes Activity’s onStop() and onSaveInstanceState(). According to the source code, you can also find that onSaveInstanceState() may be invoked before or after onStop() depends on Android OS version.

Source: http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/ActivityThread.java#4801

According above code snippet, it shows that ActivityClientRecord’s state filed is assigned by an empty Bundle object which will be passed to onSaveInstanceState() as its parameter(ie. onSaveInstanceState (Bundle outState)).

So far, we know that the Bundle object which passed to onSaveInstanceState() is an empty Bundle object without any data, and it will also be sent to Android system by binder transaction. And in this issue, the Bundle object contains too many data and cause TransactionTooLargeException. So we keep tracing the source code to find out what data will be stored to the Bundle object.

Source: https://android.googlesource.com/platform/frameworks/support/+/androidx-1.0-dev/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java?autodive=0%2F#588

Here we start to trace the default implementation of Activity’s onSaveInstanceState()for finding more clues. According to above code snippet of FragmentActivity.java, it indicates the value of android:support:fragment in Bundle object comes from mFragment.saveAllState(). Let’s keep tracing saveAllSate() to see what does it do.

Source: https://android.googlesource.com/platform/frameworks/support/+/androidx-1.0-dev/fragment/src/main/java/androidx/fragment/app/FragmentManager.java?autodive=0%2F#2938

saveAllState() is the core logic of saving state of fragments in an Activity. mFragmentStore is a member field of FragmentManager that is used to store all fragment instances which are added to the Activity. There are two array lists in mFragmentStore for storing added fragments called mActive and mAdded, so above flow will go through both of them to collect the data that need to be kept from all added fragments. And last, these data will be set to FragmentManagerState which is a data class implements Parcelable interface and put to Bundle object with the key android:support:fragment. You may wonder that why does saveActiveFragments() return a list of FragmentState which is also a data class that implements Parcelable interface, but saveAddedFragments()only returns a list of string. The reason is that mActive list is a superset of mAdded list that includes all fragments referenced by any FragmentTransaction objects in the back-stack, so framework saves complete information for the fragments in mActive list, but only save string information for the fragments in mAdded list. For more details about mActive and mAdded, you can refer to here.

https://android.googlesource.com/platform/frameworks/support/+/androidx-1.0-dev/fragment/src/main/java/androidx/fragment/app/FragmentState.java?autodive=0%2F#27

Now we can check what data is kept by FragmentState. According to above code snippet, we found that most of data field are primitive type which should not increase the data size too much, except the mArgument whose type is Bundle and assigned from Fragment’s argument object.

After tracing the source code, we can almost confirm that the issue is related to some Fragment put too much data in its Argument field, so we can review our implementation again to check what data in Fragment’s Argument causes this problem.

Below diagram illustrates the overall picture of above code flow.

Invalidation

Before reviewing the application code, I would like to introduce a useful tool, toolargetool, which can help to debug TransactionTooLargeException. It hooks into Activity’s and Fragment’s life-cycle and break down the Bundle object filled by onSaveInstanceState(), and print debug information in logcat.

Above screenshot shows the result printed by the tool, it indicates the data in Fragment’s bundle object whose key is _store has conspicuous data size and exists in many different fragments in the host Activity, and causes overall data size exceed the limitation. Until now, we have already figured out why and how the issue occurs, and we even know which fragment cause this issue. So we can start to fix it.

After reviewing the application code, there are three fragments in the Activity, and each of them put the same large data to Bundle object and set the key name to_store and pass the Bundle object to fragment instance by setArguments(Bundle args) like above code snippet.

So we just try to fetch the data form other source in onCreate() callback of fragment, instead of passing the huge data through setArgument(Bundle args) , then the issue is gone.

Conclusion

Android suggests that fragment should only have default constructor without any parameter, because Android framework needs to re-initiate the fragment instance by calling the default constructor if application is back to foreground from background and killed by Android system before. So Android provides setArguments(Bundle args) to give developer a simple way to pass parameters to fragment’s instance. At face value, this function should not be related to any binder transaction, but actually, the data put into the fragment’s argument object will also be saved to/restored from Android system automatically.

The thing we learn from this issue is that we should not pass huge data (ex. bitmap, file content or data set with large size) through setArguments(Bundle args), instead, the right way to do would be passing file reference or implement data fetch mechanism when fragment is initialized.

--

--