android.os.TransactionTooLargeException on Nougat
Asked Answered
C

13

80

I updated Nexus 5X to Android N, and now when I install the app (debug or release) on it I am getting TransactionTooLargeException on every screen transition that has Bundle in extras. The app is working on all other devices. The old app that is on PlayStore and has mostly same code is working on Nexus 5X. Is anyone having the same issue?

java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 592196 bytes
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3752)
   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:6077)
   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 592196 bytes
   at android.os.BinderProxy.transactNative(Native Method)
   at android.os.BinderProxy.transact(Binder.java:615)
   at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3606)
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3744)
   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:6077) 
   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) 
Collected answered 23/8, 2016 at 10:25 Comment(4)
write class where with appear. it means you have a class with parces with a lot of data.Semiology
#11451893Semiology
That is problem. in most cases I am sending just small parcelable object with few strings. I am getting this error on every activity transition. On other devices it works without problems.Spitsbergen
It has been reported as a bug, see code.google.com/p/android/issues/detail?id=212316Poirier
C
26

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.

Collected answered 28/9, 2016 at 12:10 Comment(4)
That's right answer! Since targetSdkVersion 24+ this exception throws instead of "App sent too much data in instance state, so it was ignored." Some details for those who want to know more: code.google.com/p/android/issues/detail?id=212316Popgun
This doesn't help when the app is simply backgrounded and the ViewState causes this. Does anyone have any idea how to handle this?Astatic
Just curious, what if you hold a static reference to the data in question? My case is an arraylist of POJO objects.Chaplin
@Chaplin Problem with that approach is that you should clear that memory when you go to another activity, and that can be tricky (you need to clean it onDestory, but not onDestroy when you rotate a screen). Otherwise, all your activities will have static vars with some objects that you don't need. This approach can work, but you will have a really messy app, so I would not recommend it.Spitsbergen
B
34

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.

Benco answered 24/6, 2017 at 16:14 Comment(6)
I tried implementing your library, but it didn't work. Still get the same crash.Recor
I'd be curious to see your implementation. Feel free to leave details as an Issue on the library. I haven't had any reports of this when properly used.Benco
I followed the instructions found on the libraries github page. Do I need to add anything else in the code?Recor
That depends on how you were saving state previously. If you were using something like Icepick then there shouldn't be any extra steps. If you were not, though, there's likely some code reorganization you'll need to do to start using a library like that first. If you posted some more information with a code sample as a Issue on the Github project I could see if there's anything wrong with your setup.Benco
I wasn't using anything. I just copied your code from gitlab and that's it.Recor
If you weren't already using Icepick (or something like it), that's not enough. As mentioned in the documentation: "Bridge is intended as a simple wrapper for annotation-based state saving libraries like Icepick, Android-State, and Icekick." There are ways to get Bridge to work without using one of those, but it ends up being more work than it would to just updating your app to use one of those.Benco
C
26

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.

Collected answered 28/9, 2016 at 12:10 Comment(4)
That's right answer! Since targetSdkVersion 24+ this exception throws instead of "App sent too much data in instance state, so it was ignored." Some details for those who want to know more: code.google.com/p/android/issues/detail?id=212316Popgun
This doesn't help when the app is simply backgrounded and the ViewState causes this. Does anyone have any idea how to handle this?Astatic
Just curious, what if you hold a static reference to the data in question? My case is an arraylist of POJO objects.Chaplin
@Chaplin Problem with that approach is that you should clear that memory when you go to another activity, and that can be tricky (you need to clean it onDestory, but not onDestroy when you rotate a screen). Otherwise, all your activities will have static vars with some objects that you don't need. This approach can work, but you will have a really messy app, so I would not recommend it.Spitsbergen
S
24

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();
}
Synonymy answered 23/9, 2017 at 6:39 Comment(4)
Thank you @Raj...you saved my timeBoudreaux
You're not saving anything that way. Clearing any "progress" users made in your Activity is a bad practice.Areola
If your instance is important and you want to store them then you can save it before clearing, //Create instance variable, Bundle oldInstance; @Override protected void onSaveInstanceState(Bundle oldInstanceState) { super.onSaveInstanceState(oldInstanceState); //save your instance here, here oldInstance = oldInstanceState oldInstanceState.clear(); }Synonymy
You saved my whole life brother. @RajYadavLeafage
N
18

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;
}
Ns answered 3/4, 2017 at 20:14 Comment(2)
in the saveState() the bundle can be null, so if (bundle != null) bundle.putParcelableArray("states", null);Luffa
@IKK828 Thanks a lot for your answer, you saved my day! Works like a charm :)Wisniewski
B
12

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

Bluepencil answered 14/6, 2017 at 8:36 Comment(2)
Thanks David . This tool really helped me in finding the root cause of the issue. You rock :)Ferullo
Welcome @NitinMesta, :)Bluepencil
C
8

I face the similar issue. The issue and scenario are little different and I fix it in the following way. Please check the scenario and solution.

Scenario: I got a weird bug from the customer in the Google Nexus 6P device(7 OS) as my application will crash after 4 hours of working. Later I identify that it's throwing the similar (android.os.TransactionTooLargeException:) exception.

Solution: The log was not pointing any particular class in the application and later I found that this is happening because of keeping the back stack of fragments. In my case, 4 fragments are added to the back stack repeatedly with the help of an auto screen movement animation. So I override the onBackstackChanged() as mention below.

 @Override
    public void onBackStackChanged() {
        try {
            int count = mFragmentMngr.getBackStackEntryCount();
            if (count > 0) {
                if (count > 30) {
                    mFragmentMngr.popBackStack(1, FragmentManager.POP_BACK_STACK_INCLUSIVE);
                    count = mFragmentMngr.getBackStackEntryCount();
                }
                FragmentManager.BackStackEntry entry = mFragmentMngr.getBackStackEntryAt(count - 1);
                mCurrentlyLoadedFragment = Integer.parseInt(entry.getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

If the stack exceeds the limit, it will automatically pop to initial fragment. I hope somebody will help this answer because the exception and stack trace logs are same. So whenever this issue happens, please check the back stack count, if you are using Fragments and back stack.

Contravene answered 23/12, 2016 at 11:27 Comment(1)
This solution seems to handle the TransactionTooLargeException and FAILED BINDER TRANSACTION exceptions I have. Would like to know what mCUrrentlyLoadedFragment is, and why we need to parse entry.getName() to it. Thanks.Maiamaiah
P
6

In my case, I got that exception inside a fragment because one of its arguments was a very large string that I forgot to delete it (I only used that large string inside the onViewCreated() method). So, to solve this, i simply deleted that argument. In your case, you have to clear or nullify any suspicious field before call onPause().

Activity code

Fragment fragment = new Fragment();
Bundle args = new Bundle();
args.putString("extremely large string", data.getValue());
fragment.setArguments(args);

Fragment code

@Override 
public void onViewCreated(View view, Bundle savedInstanceState) {

    String largeString = arguments.get("extremely large string");       
    //Do Something with the large string   
    arguments.clear() //I forgot to execute this  
}
Pertussis answered 2/4, 2018 at 5:57 Comment(2)
Thanks a lot, worked for me, was looking for this problem since long, tried everything, and finally this worked.Bushman
Wouldn't the arguments clear after the fragment is removed??? Why isnt this part of the logic in fragments...Jaggy
M
3

The problem in my app was that I was trying to save too much into the savedInstanceState, the solution was to identify exactly which data should be saved at the right time. Basically look carefully into your onSaveInstanceState to make sure you don't stretch it:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current state
    // Check carefully what you're adding into the savedInstanceState before saving it
    super.onSaveInstanceState(savedInstanceState);
}
Monomania answered 10/3, 2017 at 14:38 Comment(1)
I had the same problem and finally I got fixed it removing some data I was saving into the savedInstanceState. Be careful specially with outState.putParcelableArrayList(... or Parcelable objects that contains parcelable lists.Ziska
A
2

In my case, I used TooLargeTool to track where the issue was coming from and I found out the android:support:fragments key in the Bundle from my onSaveInstanceState used to reach almost 1mb when the app crashed. So the solution was like:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.remove("android:support:fragments");
}

By doing that, I avoided to save all fragments' states and kept with other things that need to be saved.

Atomizer answered 26/4, 2019 at 18:44 Comment(2)
Where did you add the above code snippet? In the parent activity or the fragment itself. Also, is the above solution working for you in order to eliminate the Transaction Too Large Exception?Caerphilly
@JatinJha I added that code in the activity and it prevented to have TransactionTooLargeException. If that didn't work for you, it's better you check what is doing by using github.com/guardian/toolargetoolAtomizer
A
1

I faced the same issue. My workaround offloads savedInstanceState to files in cache dir.

I made the following utility class.

package net.cattaka.android.snippets.issue;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * To parry BUG of Android N. https://code.google.com/p/android/issues/detail?id=212316
 * <p>
 * Created by cattaka on 2017/01/12.
 */
public class Issue212316Parrier {
    public static final String DEFAULT_NAME = "Issue212316Parrier";
    private static final String KEY_STORED_BUNDLE_ID = "net.cattaka.android.snippets.issue.Issue212316Parrier.KEY_STORED_BUNDLE_ID";

    private String mName;
    private Context mContext;
    private String mAppVersionName;
    private int mAppVersionCode;
    private SharedPreferences mPreferences;
    private File mDirForStoredBundle;

    public Issue212316Parrier(Context context, String appVersionName, int appVersionCode) {
        this(context, appVersionName, appVersionCode, DEFAULT_NAME);
    }

    public Issue212316Parrier(Context context, String appVersionName, int appVersionCode, String name) {
        mName = name;
        mContext = context;
        mAppVersionName = appVersionName;
        mAppVersionCode = appVersionCode;
    }

    public void initialize() {
        mPreferences = mContext.getSharedPreferences(mName, Context.MODE_PRIVATE);

        File cacheDir = mContext.getCacheDir();
        mDirForStoredBundle = new File(cacheDir, mName);
        if (!mDirForStoredBundle.exists()) {
            mDirForStoredBundle.mkdirs();
        }

        long lastStoredBundleId = 1;
        boolean needReset = true;
        String fingerPrint = (Build.FINGERPRINT != null) ? Build.FINGERPRINT : "";
        needReset = !fingerPrint.equals(mPreferences.getString("deviceFingerprint", null))
                || !mAppVersionName.equals(mPreferences.getString("appVersionName", null))
                || (mAppVersionCode != mPreferences.getInt("appVersionCode", 0));
        lastStoredBundleId = mPreferences.getLong("lastStoredBundleId", 1);

        if (needReset) {
            clearDirForStoredBundle();

            mPreferences.edit()
                    .putString("deviceFingerprint", Build.FINGERPRINT)
                    .putString("appVersionName", mAppVersionName)
                    .putInt("appVersionCode", mAppVersionCode)
                    .putLong("lastStoredBundleId", lastStoredBundleId)
                    .apply();
        }
    }

    /**
     * Call this from {@link android.app.Activity#onCreate(Bundle)}, {@link android.app.Activity#onRestoreInstanceState(Bundle)} or {@link android.app.Activity#onPostCreate(Bundle)}
     */
    public void restoreSaveInstanceState(@Nullable Bundle savedInstanceState, boolean deleteStoredBundle) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_BUNDLE_ID)) {
                long storedBundleId = savedInstanceState.getLong(KEY_STORED_BUNDLE_ID);
                File storedBundleFile = new File(mDirForStoredBundle, storedBundleId + ".bin");
                Bundle storedBundle = loadBundle(storedBundleFile);
                if (storedBundle != null) {
                    savedInstanceState.putAll(storedBundle);
                }
                if (deleteStoredBundle && storedBundleFile.exists()) {
                    storedBundleFile.delete();
                }
            }
        }
    }

    /**
     * Call this from {@link android.app.Activity#onSaveInstanceState(Bundle)}
     */
    public void saveInstanceState(Bundle outState) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (outState != null) {
                long nextStoredBundleId = mPreferences.getLong("lastStoredBundleId", 1) + 1;
                mPreferences.edit().putLong("lastStoredBundleId", nextStoredBundleId).apply();
                File storedBundleFile = new File(mDirForStoredBundle, nextStoredBundleId + ".bin");
                saveBundle(outState, storedBundleFile);
                outState.clear();
                outState.putLong(KEY_STORED_BUNDLE_ID, nextStoredBundleId);
            }
        }
    }

    private void saveBundle(@NonNull Bundle bundle, @NonNull File storedBundleFile) {
        byte[] blob = marshall(bundle);
        OutputStream out = null;
        try {
            out = new GZIPOutputStream(new FileOutputStream(storedBundleFile));
            out.write(blob);
            out.flush();
            out.close();
        } catch (IOException e) {
            // ignore
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }

    @Nullable
    private Bundle loadBundle(File storedBundleFile) {
        byte[] blob = null;
        InputStream in = null;
        try {
            in = new GZIPInputStream(new FileInputStream(storedBundleFile));
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            int n;
            byte[] buffer = new byte[1024];
            while ((n = in.read(buffer)) > -1) {
                bout.write(buffer, 0, n);   // Don't allow any extra bytes to creep in, final write
            }
            bout.close();
            blob = bout.toByteArray();
        } catch (IOException e) {
            // ignore
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }

        try {
            return (blob != null) ? (Bundle) unmarshall(blob) : null;
        } catch (Exception e) {
            return null;
        }
    }

    private void clearDirForStoredBundle() {
        for (File file : mDirForStoredBundle.listFiles()) {
            if (file.isFile() && file.getName().endsWith(".bin")) {
                file.delete();
            }
        }
    }


    @NonNull
    private static <T extends Parcelable> byte[] marshall(@NonNull final T object) {
        Parcel p1 = Parcel.obtain();
        p1.writeValue(object);

        byte[] data = p1.marshall();
        p1.recycle();
        return data;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    private static <T extends Parcelable> T unmarshall(@NonNull byte[] bytes) {
        Parcel p2 = Parcel.obtain();
        p2.unmarshall(bytes, 0, bytes.length);
        p2.setDataPosition(0);
        T result = (T) p2.readValue(Issue212316Parrier.class.getClassLoader());
        p2.recycle();
        return result;
    }
}

Full codes: https://github.com/cattaka/AndroidSnippets/pull/37

I worry about that Parcel#marshall should not be used for persistent. But, I don't have any other idea.

Accommodation answered 12/1, 2017 at 6:25 Comment(2)
Can you please take a look at my updated answer. I accidentally found that solution. At first glance it looks strange, but it works really nice.Spitsbergen
Your solution is looks good. But I think it cannot restore when process was killed by system. However it may not big problem because it is a edge case.Accommodation
E
1

None of the above answers worked for me, the reason of the issue was quite simple as stated by some I was using FragmentStatePagerAdapter and its saveState method saves the state of the fragments, because one of my fragment was quite large , so saving of this fragment leads to this TransactionTooLargeExecption.

I tried overriding the saveState method in my implementation of pager as stated by @IK828, but this couldn't resolve the crash.

My fragment was having an EditText which used to hold very large text, which was the culprit of the issue in my case, so simply in onPause() of fragment, I set the edittext text to empty string. ie:

@Override
    public void onPause() {
       edittext.setText("");
}

Now when FragmentStatePagerAdapter will try to saveState, this large chunk of text will not be there to consume bigger part of it, hence resolves the crash.

In your case you need to find whatever is the culprit, it could be an ImageView with some bitmap, a TextView with huge chunk of text or any other high memory consuming view, you need to free it's memory, you may set imageview.setImageResource(null) or similar in onPause() of your fragment.

update : onSaveInstanceState is better place for the purpose before calling super like:

@Override
    public void onSaveInstanceState(Bundle outState) {
        edittext.setText("");
        super.onSaveInstanceState(outState);
    }

or as pointed by @Vladimir you can use android:saveEnabled="false" or view.setSaveEnabled(false); on the view or custom view and make sure to set the text back in onResume otherwise it will be empty when Activity resumes.

Encyclopedic answered 20/11, 2017 at 18:45 Comment(1)
A better solution would be to use android:saveEnabled="false" on a particular View. You can read more about it here.Spitsbergen
C
0

Just Override this method on your activity :

@Override
protected void onSaveInstanceState(Bundle outState) {
    // below line to be commented to prevent crash on nougat.
    // http://blog.sqisland.com/2016/09/transactiontoolargeexception-crashes-nougat.html
    //
    //super.onSaveInstanceState(outState);
}

Go to https://code.google.com/p/android/issues/detail?id=212316#makechanges for more info.

Cheltenham answered 13/12, 2016 at 13:53 Comment(4)
it might help if you could add some more info that the users could see before they click the linkAgnusago
This answer alone is not helpfulCrystallize
transactiontoolargeexception arise when saving large data into SaveInstanceState. By this code it will prevent saving data.Projectionist
It's not a good solution because you might need to store something from your activityAtomizer
I
0

As the Android N change the behavior and throw TransactionTooLargeException instead of logging the error.

     try {
            if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity);
            ActivityManagerNative.getDefault().activityStopped(
                activity.token, state, persistentState, description);
        } catch (RemoteException ex) {
            if (ex instanceof TransactionTooLargeException
                    && activity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
                Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
                return;
            }
            throw ex.rethrowFromSystemServer();
        }

my solution is to hook the ActivityMangerProxy instance and try catch the activityStopped method.

Here is the code:

private boolean hookActivityManagerNative() {
    try {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Field singletonField = ReflectUtils.findField(loader.loadClass("android.app.ActivityManagerNative"), "gDefault");
        ReflectUtils.ReflectObject singletonObjWrap = ReflectUtils.wrap(singletonField.get(null));
        Object realActivityManager = singletonObjWrap.getChildField("mInstance").get();
        Object fakeActivityManager = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{loader.loadClass("android.app.IActivityManager")}, new ActivityManagerHook(realActivityManager));
        singletonObjWrap.setChildField("mInstance", fakeActivityManager);
        return true;
    } catch (Throwable e) {
        AppHolder.getThirdPartUtils().markException(e);
        return false;
    }
}

private static class ActivityManagerHook implements InvocationHandler {

    private Object origin;

    ActivityManagerHook(Object origin) {
       this.origin = origin;
    }

    public Object getOrigin() {
        return origin;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        switch (method.getName()) {
            //ActivityManagerNative.getDefault().activityStopped(activity.token, state, persistentState, description);
            case "activityStopped": {
                try {
                    return method.invoke(getOrigin(), args);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
        return method.invoke(getOrigin(), args);
    }
}

And the reflect helper class is

public class ReflectUtils {

private static final HashMap<String, Field> fieldCache = new HashMap<>();
private static final HashMap<String, Method> methodCache = new HashMap<>();

public static Field findField(Class<?> clazz, String fieldName) throws Throwable {
    String fullFieldName = clazz.getName() + '#' + fieldName;

    if (fieldCache.containsKey(fullFieldName)) {
        Field field = fieldCache.get(fullFieldName);
        if (field == null)
            throw new NoSuchFieldError(fullFieldName);
        return field;
    }

    try {
        Field field = findFieldRecursiveImpl(clazz, fieldName);
        field.setAccessible(true);
        fieldCache.put(fullFieldName, field);
        return field;
    } catch (NoSuchFieldException e) {
        fieldCache.put(fullFieldName, null);
        throw new NoSuchFieldError(fullFieldName);
    }
}


private static Field findFieldRecursiveImpl(Class<?> clazz, String fieldName) throws NoSuchFieldException {
    try {
        return clazz.getDeclaredField(fieldName);
    } catch (NoSuchFieldException e) {
        while (true) {
            clazz = clazz.getSuperclass();
            if (clazz == null || clazz.equals(Object.class))
                break;

            try {
                return clazz.getDeclaredField(fieldName);
            } catch (NoSuchFieldException ignored) {
            }
        }
        throw e;
    }
}


public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) throws Throwable {
    String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";

    if (methodCache.containsKey(fullMethodName)) {
        Method method = methodCache.get(fullMethodName);
        if (method == null)
            throw new NoSuchMethodError(fullMethodName);
        return method;
    }

    try {
        Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
        method.setAccessible(true);
        methodCache.put(fullMethodName, method);
        return method;
    } catch (NoSuchMethodException e) {
        methodCache.put(fullMethodName, null);
        throw new NoSuchMethodError(fullMethodName);
    }
}


/**
 * Returns an array of the given classes.
 */
public static Class<?>[] getClassesAsArray(Class<?>... clazzes) {
    return clazzes;
}

private static String getParametersString(Class<?>... clazzes) {
    StringBuilder sb = new StringBuilder("(");
    boolean first = true;
    for (Class<?> clazz : clazzes) {
        if (first)
            first = false;
        else
            sb.append(",");

        if (clazz != null)
            sb.append(clazz.getCanonicalName());
        else
            sb.append("null");
    }
    sb.append(")");
    return sb.toString();
}

/**
 * Retrieve classes from an array, where each element might either be a Class
 * already, or a String with the full class name.
 */
private static Class<?>[] getParameterClasses(ClassLoader classLoader, Object[] parameterTypes) throws ClassNotFoundException {
    Class<?>[] parameterClasses = null;
    for (int i = parameterTypes.length - 1; i >= 0; i--) {
        Object type = parameterTypes[i];
        if (type == null)
            throw new ClassNotFoundException("parameter type must not be null", null);

        if (parameterClasses == null)
            parameterClasses = new Class<?>[i + 1];

        if (type instanceof Class)
            parameterClasses[i] = (Class<?>) type;
        else if (type instanceof String)
            parameterClasses[i] = findClass((String) type, classLoader);
        else
            throw new ClassNotFoundException("parameter type must either be specified as Class or String", null);
    }

    // if there are no arguments for the method
    if (parameterClasses == null)
        parameterClasses = new Class<?>[0];

    return parameterClasses;
}

public static Class<?> findClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
    if (classLoader == null)
        classLoader = ClassLoader.getSystemClassLoader();
    return classLoader.loadClass(className);
}


public static ReflectObject wrap(Object object) {
    return new ReflectObject(object);
}


public static class ReflectObject {

    private Object object;

    private ReflectObject(Object o) {
        this.object = o;
    }

    public ReflectObject getChildField(String fieldName) throws Throwable {
        Object child = ReflectUtils.findField(object.getClass(), fieldName).get(object);
        return ReflectUtils.wrap(child);
    }

    public void setChildField(String fieldName, Object o) throws Throwable {
        ReflectUtils.findField(object.getClass(), fieldName).set(object, o);
    }

    public ReflectObject callMethod(String methodName, Object... args) throws Throwable {
        Class<?>[] clazzs = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            clazzs[i] = args.getClass();
        }
        Method method = ReflectUtils.findMethodExact(object.getClass(), methodName, clazzs);
        return ReflectUtils.wrap(method.invoke(object, args));
    }

    public <T> T getAs(Class<T> clazz) {
        return (T) object;
    }

    public <T> T get() {
        return (T) object;
    }
}
}
Infield answered 21/4, 2017 at 1:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.