I got a TransactionTooLargeException
. Not reproducible. In the docs it says
The Binder transaction failed because it was too large.
D
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;
}
For those who bitterly disappointed in search of answer of why the TransactionTooLargeException apears, try to check how much information you save in instance state.
On compile/targetSdkVersion <= 23 we have only internal warning about large size of saved state, but nothing is crashed:
E/ActivityThread: App sent too much data in instance state, so it was ignored
android.os.TransactionTooLargeException: data parcel size 713856 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(Binder.java:615)
at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3604)
at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3729)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6044)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
But on compile/targetSdkVersion >= 24 we have real RuntimeException crash in this case:
java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 713860 bytes
at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3737)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6044)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
Caused by: android.os.TransactionTooLargeException: data parcel size 713860 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(Binder.java:615)
at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3604)
at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3729)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6044)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
What to do?
Save data in local database and keep only id's in instance state which you can use to retrieve this data.
Try to use EventBus
or ContentProvider
like solution.
If you are in the same process(normally all your activities would be), try to use EventBus
, cause in process data exchange does NOT need a somewhat buffer, so you do not need to worry about your data is too large. (You can just use method call to pass data indeed, and EventBus hide the ugly things)
Here is the detail:
// one side
startActivity(intentNotTooLarge);
EventBus.getDefault().post(new FooEvent(theHugeData));
// the other side
@Subscribe public void handleData(FooEvent event) { /* get and handle data */ }
If the two sides of Intent are not in the same process, try somewhat ContentProvider
.
See TransactionTooLargeException
The Binder transaction failed because it was too large.
During a remote procedure call, the arguments and the return value of the call are transferred as Parcel objects stored in the Binder transaction buffer. If the arguments or the return value are too large to fit in the transaction buffer, then the call will fail and TransactionTooLargeException will be thrown.
Issue is resolved by:
Bundle bundle = new Bundle();
bundle.putSerializable("data", bigdata);
...
CacheHelper.saveState(bundle,"DATA");
Intent intent = new Intent(mActivity, AActivity.class);
startActivity(intent, bb);// do not put data to intent.
In Activity:
@Override
protected void onCreate(Bundle savedInstanceState) {
Bundle bundle = CacheHelper.getInstance().loadState(Constants.DATA);
if (bundle != null){
Intent intent = getIntent();
intent.putExtras(bundle);
}
getIntent().getExtra(..);
....
}
@Override
protected void onDestroy() {
super.onDestroy();
CacheHelper.clearState("DATA");
}
public class CacheHelper {
public static void saveState(Bundle savedInstanceState, String name) {
Bundle saved = (Bundle) savedInstanceState.clone();
save(name, saved);
}
public Bundle loadState(String name) {
Object object = load(name);
if (object != null) {
Bundle bundle = (Bundle) object;
return bundle;
}
return null;
}
private static void save(String fileName, Bundle object) {
try {
String path = StorageUtils.getFullPath(fileName);
File file = new File(path);
if (file.exists()) {
file.delete();
}
FileOutputStream fos = new FileOutputStream(path, false);
Parcel p = Parcel.obtain(); //creating empty parcel object
object.writeToParcel(p, 0); //saving bundle as parcel
//parcel.writeBundle(bundle);
fos.write(p.marshall()); //writing parcel to file
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static Bundle load(String fileName) {
try {
String path = StorageUtils.getFullPath(fileName);
FileInputStream fis = new FileInputStream(path);
byte[] array = new byte[(int) fis.getChannel().size()];
fis.read(array, 0, array.length);
Parcel parcel = Parcel.obtain(); //creating empty parcel object
parcel.unmarshall(array, 0, array.length);
parcel.setDataPosition(0);
Bundle out = parcel.readBundle();
out.putAll(out);
fis.close();
parcel.recycle();
return out;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void clearState(Activity ac) {
String name = ac.getClass().getName();
String path = StorageUtils.getFullPath(name);
File file = new File(path);
if (file.exists()) {
file.delete();
}
}
}
There isn't one specific cause of this problem.For me, in my Fragment class I was doing this:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View rootView = inflater.inflate(R.layout.snacks_layout, container); //<-- notice the absence of the false argument
return rootView;
}
instead of this:
View rootView = inflater.inflate(R.layout.softs_layout, container, false);
I got this in my syncadapter when trying to bulkInsert a large ContentValues[]. I decided to fix it as follows:
try {
count = provider.bulkInsert(uri, contentValueses);
} catch (TransactionTooLarge e) {
int half = contentValueses.length/2;
count += provider.bulkInsert(uri, Arrays.copyOfRange(contentValueses, 0, half));
count += provider.bulkInsert(uri, Arrays.copyOfRange(contentValueses, half, contentValueses.length));
}