78 votes

Android.os.TransactionTooLargeException sur Nougat

J'ai mis à jour le Nexus 5X vers Android N, et maintenant quand j'installe l'application (debug ou release) sur celui-ci, je reçois TransactionTooLargeException sur chaque transition d'écran qui a Bundle en plus. L'application fonctionne sur tous les autres appareils. L'ancienne application qui est sur le PlayStore et qui a pratiquement le même code fonctionne sur le Nexus 5X. Quelqu'un rencontre-t-il le même problème ?

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)

0 votes

Écrire la classe où avec apparaissent. cela signifie que vous avez une classe avec des parces avec beaucoup de données.

0 votes

1 votes

C'est là le problème. Dans la plupart des cas, je n'envoie qu'un petit objet compressible avec peu de chaînes de caractères. J'obtiens cette erreur à chaque transition d'activité. Sur d'autres appareils, cela fonctionne sans problème.

31voto

Brian Yencho Points 589

Chaque fois que vous voyez TransactionTooLargeException qui se produit lorsqu'un Activity est en train de s'arrêter, cela signifie que la Activity essayait d'envoyer son état sauvegardé Bundles au système d'exploitation du système afin de le conserver en toute sécurité pour le restaurer ultérieurement (après un changement de configuration ou la mort d'un processus), mais qu'un ou plusieurs des éléments suivants soient conservés Bundles qu'il a envoyés étaient trop grands. Il existe une limite maximale d'environ 1 Mo pour toutes les transactions de ce type qui se produisent en même temps, et cette limite peut être atteinte même si aucune transaction de ce type n'est envoyée. Bundle dépasse cette limite.

Le principal coupable ici est généralement de sauvegarder trop de données à l'intérieur onSaveInstanceState de l'un ou l'autre des Activity ou tout Fragments organisé par le Activity . Cela se produit généralement lors de la sauvegarde d'un objet particulièrement volumineux, tel qu'un fichier de type Bitmap mais cela peut également se produire lors de l'envoi de grandes quantités de données plus petites, comme des listes de Parcelable objets. L'équipe Android a précisé à de nombreuses reprises que seules de petites quantités de données liées à la vue devaient être sauvegardées dans les objets suivants onSavedInstanceState . Cependant, les développeurs ont souvent sauvegardé des pages de données réseau afin que les changements de configuration apparaissent aussi fluides que possible en n'ayant pas à récupérer à nouveau les mêmes données. Depuis la Google I/O 2017, l'équipe Android a clairement indiqué que l'architecture préférée pour une application Android enregistre les données de réseau.

  • en mémoire afin qu'il puisse être facilement réutilisé lors de changements de configuration.
  • sur le disque afin qu'il puisse être facilement restauré après la mort du processus et des sessions d'applications.

Leur nouvelle ViewModel cadre et Room sont destinés à aider les développeurs à s'adapter à ce modèle. Si votre problème est de sauvegarder trop de données dans des fichiers onSaveInstanceState la mise à jour vers une architecture comme celle-ci en utilisant ces outils devrait résoudre votre problème.

Personnellement, avant d'effectuer une mise à jour vers ce nouveau modèle, j'aimerais prendre mes applications existantes et contourner la TransactionTooLargeException pendant ce temps. J'ai écrit une bibliothèque rapide pour faire cela : https://github.com/livefront/bridge . Il utilise les mêmes idées générales de restauration de l'état à partir de la mémoire lors des changements de configuration et à partir du disque après la mort du processus, plutôt que d'envoyer tout cet état au système d'exploitation par l'intermédiaire de l'application onSaveInstanceState mais ne nécessite qu'un minimum de modifications de votre code existant pour être utilisé. Toute stratégie qui répond à ces deux objectifs devrait vous permettre d'éviter l'exception, sans pour autant sacrifier votre capacité à sauvegarder l'état.

Une dernière remarque ici : la seule raison pour laquelle vous voyez cela sur Nougat+ est qu'à l'origine, si la limite de transaction du binder était dépassée, le processus d'envoi de l'état sauvegardé à l'OS échouait silencieusement avec seulement cette erreur apparaissant dans Logcat :

! !! TRANSACTION DE LIANT RATÉE ! !!

Dans Nougat, cette défaillance silencieuse est passée à un crash dur. À leur crédit, c'est quelque chose que l'équipe de développement a documenté dans les notes de mise à jour de Nougat :

De nombreuses API de plate-forme ont maintenant commencé à vérifier les charges utiles volumineuses envoyées par les transactions de Binder, et le système renvoie maintenant les TransactionTooLargeExceptions comme RuntimeExceptions, au lieu de les consigner ou de les supprimer silencieusement. Un exemple courant est le stockage d'une trop grande quantité de données dans Activity.onSaveInstanceState(), ce qui entraîne l'envoi d'une exception d'exécution par ActivityThread.StopInfo lorsque votre application cible Android 7.0.

0 votes

J'ai essayé d'implémenter votre bibliothèque, mais ça n'a pas marché. J'obtiens toujours le même plantage.

0 votes

Je serais curieux de voir votre mise en œuvre. N'hésitez pas à laisser les détails en tant que problème sur la bibliothèque. Je n'ai eu aucun rapport à ce sujet lorsqu'il est utilisé correctement.

0 votes

J'ai suivi les instructions trouvées sur la page github des bibliothèques. Dois-je ajouter autre chose dans le code ?

25voto

Vladimir Jovanović Points 1338

En fin de compte, mon problème concernait les éléments qui étaient enregistrés surSaveInstance, et non ceux qui étaient envoyés à l'activité suivante. J'ai supprimé toutes les sauvegardes où je ne peux pas contrôler la taille des objets (réponses réseau), et maintenant ça marche.

Mise à jour 2 :

Google fournit maintenant AndroidX ViewModel qui est basé sur la même technologie que les Fragments retenus mais beaucoup plus facile à utiliser. ViewModel est désormais une approche privilégiée.

Mise à jour 1 :

Pour conserver de gros morceaux de données, Google suggère de le faire avec Fragment qui conserve l'instance. L'idée est de créer un Fragment vide sans vue avec tous les champs nécessaires, qui seraient autrement sauvegardés dans le Bundle. Ajouter setRetainInstance(true); à la méthode onCreate du Fragment. Et ensuite, sauvegarder les données dans le fragment lors du onDestroy de l'activité et les charger lors du onCreate. Voici un exemple d'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());
    }
}

Un exemple de 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;
    }
}

Pour en savoir plus, vous pouvez lire aquí .

5 votes

C'est la bonne réponse ! Depuis targetSdkVersion 24+ cette exception se lève au lieu de "L'application a envoyé trop de données dans l'état d'instance, donc il a été ignoré". Quelques détails pour ceux qui veulent en savoir plus : code.google.com/p/Android/issues/detail?id=212316

2 votes

Cela n'aide pas lorsque l'application est simplement en arrière-plan et que le ViewState provoque ce problème. Quelqu'un a-t-il une idée de la façon de gérer ce problème ?

0 votes

Par curiosité, que se passe-t-il si vous détenez une référence statique aux données en question ? Dans mon cas, il s'agit d'une liste de tableaux d'objets POJO.

21voto

Raj Yadav Points 321

J'ai fait un essai, et finalement cela a résolu mon problème. Ajoutez ceci à votre Activity

@Override
protected void onSaveInstanceState(Bundle oldInstanceState) {
    super.onSaveInstanceState(oldInstanceState);
    oldInstanceState.clear();
}

2 votes

Merci @Raj...vous avez sauvé mon temps

1 votes

Vous n'économisez rien de cette façon. L'effacement de tout "progrès" des utilisateurs dans vos Activity est une mauvaise pratique.

0 votes

Si votre instance est importante et que vous souhaitez la conserver, vous pouvez la sauvegarder avant de l'effacer, //Create instance variable, Bundle oldInstance; @Override protected void onSaveInstanceState(Bundle oldInstanceState) { super.onSaveInstanceState(oldInstanceState); //save your instance here, here oldInstance = oldInstanceState oldInstanceState.clear(); }

18voto

IK828 Points 534

L'exception TransactionTooLargeException nous a tourmenté pendant environ 4 mois maintenant, et nous avons finalement résolu le problème !

Ce qui se passe, c'est que nous utilisons un FragmentStatePagerAdapter dans un ViewPager. L'utilisateur feuilletait et créait plus de 100 fragments (il s'agit d'une application de lecture).

Bien que nous gérions correctement les fragments dans destroyItem(), dans l'implémentation d'Androïdes de FragmentStatePagerAdapter il y a un bug, où il garde une référence à la liste suivante :

private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();

Et lorsque le FragmentStatePagerAdapter d'Android tente de sauvegarder l'état, il appelle la fonction

@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;
}

Comme vous pouvez le constater, même si vous gérez correctement les fragments dans la sous-classe FragmentStatePagerAdapter, la classe de base stockera toujours un Fragment.SavedState pour chaque fragment créé. L'exception TransactionTooLargeException se produirait lorsque ce tableau serait vidé dans un parcelableArray et le système d'exploitation n'apprécierait pas qu'il y ait plus de 100 éléments.

La solution pour nous a donc été de remplacer la méthode saveState() et de ne rien stocker pour les "états".

@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;
}

2 votes

Dans la saveState() le paquet peut être null, donc if (bundle != null) bundle.putParcelableArray("states", null) ;

0 votes

@IKK828 Merci beaucoup pour votre réponse, vous avez sauvé ma journée ! Ça marche comme sur des roulettes :)

11voto

David Cheung Points 2327

Je suis également confronté à ce problème sur mes appareils Nougat. Mon application utilise un fragment avec une vue pager qui contient 4 fragments. J'ai passé quelques grands arguments de construction aux 4 fragments qui ont causé le problème.

J'ai tracé la taille de Bundle en provoquant cela avec l'aide de Outil trop grand .

Finalement, je l'ai résolu en utilisant putSerializable sur un objet POJO qui implémente Serializable au lieu de passer un gros fichier brut String en utilisant putString pendant l'initialisation du fragment. Cette réduction de la taille de Bundle de moitié et ne jette pas le TransactionTooLargeException . Par conséquent, assurez-vous que vous ne passez pas des arguments de taille énorme à Fragment .

P.S. Problème connexe dans Google Issue Tracker : https://issuetracker.google.com/issues/37103380

3 votes

Merci David . Cet outil m'a vraiment aidé à trouver la cause profonde du problème. Vous êtes génial :)

0 votes

Bienvenue @NitinMesta, :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X