Quand peut-on utiliser des classes internes (anonymes) en toute sécurité?


324

J'ai lu quelques articles sur les fuites de mémoire dans Android et j'ai regardé cette vidéo intéressante de Google I / O sur le sujet .

Pourtant, je ne comprends pas complètement le concept, et surtout quand il est sûr ou dangereux pour les classes internes des utilisateurs à l' intérieur d'une activité .

Voici ce que j'ai compris:

Une fuite de mémoire se produit si une instance d'une classe interne survit plus longtemps que sa classe externe (une activité). -> Dans quelles situations cela peut-il arriver?

Dans cet exemple, je suppose qu'il n'y a aucun risque de fuite, car il n'y a aucun moyen que l'extension de classe anonyme OnClickListenervive plus longtemps que l'activité, non?

    final Dialog dialog = new Dialog(this);
    dialog.setContentView(R.layout.dialog_generic);
    Button okButton = (Button) dialog.findViewById(R.id.dialog_button_ok);
    TextView titleTv = (TextView) dialog.findViewById(R.id.dialog_generic_title);

    // *** Handle button click
    okButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            dialog.dismiss();
        }
    });

    titleTv.setText("dialog title");
    dialog.show();

Maintenant, cet exemple est-il dangereux et pourquoi?

// We are still inside an Activity
_handlerToDelayDroidMove = new Handler();
_handlerToDelayDroidMove.postDelayed(_droidPlayRunnable, 10000);

private Runnable _droidPlayRunnable = new Runnable() { 
    public void run() {
        _someFieldOfTheActivity.performLongCalculation();
    }
};

J'ai un doute sur le fait que la compréhension de ce sujet a à voir avec la compréhension détaillée de ce qui est conservé lorsqu'une activité est détruite et recréée.

C'est ça?

Disons que je viens de changer l'orientation de l'appareil (qui est la cause la plus courante de fuites). Quand super.onCreate(savedInstanceState)sera appelé dans mon onCreate(), cela restaurera-t-il les valeurs des champs (telles qu'elles étaient avant le changement d'orientation)? Cela restaurera-t-il également les états des classes internes?

Je réalise que ma question n'est pas très précise, mais j'apprécierais vraiment toute explication qui pourrait clarifier les choses.


14
Cet article de blog et cet article de blog contiennent de bonnes informations sur les fuites de mémoire et les classes internes. :)
Alex Lockwood

Réponses:


651

Ce que vous demandez est une question assez difficile. Bien que vous puissiez penser qu'il ne s'agit que d'une seule question, vous posez en fait plusieurs questions à la fois. Je ferai de mon mieux en sachant que je dois le couvrir et, espérons-le, d'autres se joindront à moi pour couvrir ce qui pourrait me manquer.

Classes imbriquées: introduction

Comme je ne sais pas à quel point vous êtes à l'aise avec la POO en Java, cela touchera quelques bases. Une classe imbriquée est lorsqu'une définition de classe est contenue dans une autre classe. Il existe essentiellement deux types: les classes imbriquées statiques et les classes internes. La vraie différence entre ceux-ci sont:

  • Classes imbriquées statiques:
    • Sont considérés comme "de haut niveau".
    • Ne nécessite pas la construction d'une instance de la classe conteneur.
    • Ne peut pas référencer les membres de classe contenant sans référence explicite.
    • Ont leur propre vie.
  • Classes imbriquées internes:
    • Il faut toujours construire une instance de la classe conteneur.
    • Avoir automatiquement une référence implicite à l'instance conteneur.
    • Peut accéder aux membres de la classe du conteneur sans référence.
    • La durée de vie ne devrait pas être plus longue que celle du conteneur.

Collecte des ordures et classes internes

Le garbage collection est automatique mais essaie de supprimer des objets selon qu'il pense qu'ils sont utilisés. Le garbage collector est assez intelligent, mais pas parfait. Il peut uniquement déterminer si quelque chose est utilisé, qu'il existe ou non une référence active à l'objet.

Le vrai problème ici est quand une classe interne a été maintenue en vie plus longtemps que son conteneur. Cela est dû à la référence implicite à la classe conteneur. Cela ne peut se produire que si un objet en dehors de la classe conteneur conserve une référence à l'objet interne, sans tenir compte de l'objet conteneur.

Cela peut conduire à une situation où l'objet interne est vivant (via une référence) mais les références à l'objet contenant ont déjà été supprimées de tous les autres objets. L'objet intérieur garde donc vivant l'objet contenant car il y aura toujours une référence. Le problème avec cela est qu'à moins qu'il ne soit programmé, il n'y a aucun moyen de revenir à l'objet contenant pour vérifier s'il est encore vivant.

L'aspect le plus important de cette prise de conscience est qu'il ne fait aucune différence que ce soit dans une activité ou dans un dessin. Vous devrez toujours être méthodique lorsque vous utilisez des classes internes et assurez-vous qu'elles ne survivent jamais aux objets du conteneur. Heureusement, si ce n'est pas un objet central de votre code, les fuites peuvent être faibles en comparaison. Malheureusement, ce sont quelques-unes des fuites les plus difficiles à trouver, car elles sont susceptibles de passer inaperçues jusqu'à ce que beaucoup d'entre elles aient fui.

Solutions: classes internes

  • Obtenez des références temporaires à partir de l'objet conteneur.
  • Permettez à l'objet conteneur d'être le seul à conserver des références de longue durée aux objets internes.
  • Utilisez des modèles établis tels que l'usine.
  • Si la classe interne n'a pas besoin d'accéder aux membres de la classe contenante, envisagez de la transformer en classe statique.
  • À utiliser avec prudence, qu'il s'agisse d'une activité ou non.

Activités et opinions: Introduction

Les activités contiennent beaucoup d'informations pour pouvoir être exécutées et affichées. Les activités sont définies par la caractéristique qu'elles doivent avoir une vue. Ils ont également certains gestionnaires automatiques. Que vous le spécifiiez ou non, l'activité a une référence implicite à la vue qu'elle contient.

Pour qu'une vue soit créée, elle doit savoir où la créer et si elle a des enfants pour qu'elle puisse s'afficher. Cela signifie que chaque vue a une référence à l'activité (via getContext()). De plus, chaque vue conserve des références à ses enfants (c. getChildAt()-à-d.). Enfin, chaque vue conserve une référence au bitmap rendu qui représente son affichage.

Chaque fois que vous avez une référence à une activité (ou un contexte d'activité), cela signifie que vous pouvez suivre la chaîne ENTIÈRE dans la hiérarchie de la disposition. C'est pourquoi les fuites de mémoire concernant les activités ou les vues sont si importantes. Cela peut être une tonne de mémoire qui fuit en même temps.

Activités, vues et classes internes

Compte tenu des informations ci-dessus sur les classes internes, ce sont les fuites de mémoire les plus courantes, mais aussi les plus couramment évitées. Bien qu'il soit souhaitable d'avoir une classe interne ayant un accès direct aux membres d'une classe d'activités, beaucoup sont prêts à les rendre statiques pour éviter les problèmes potentiels. Le problème avec les activités et les vues va bien plus loin que cela.

Activités, vues et contextes d'activité qui ont fait l'objet d'une fuite

Tout se résume au contexte et au cycle de vie. Certains événements (tels que l'orientation) peuvent tuer un contexte d'activité. Étant donné que tant de classes et de méthodes nécessitent un contexte, les développeurs essaient parfois de sauvegarder du code en saisissant une référence à un contexte et en le conservant. Il se trouve que bon nombre des objets que nous devons créer pour exécuter notre activité doivent exister en dehors du cycle de vie de l'activité afin de permettre à l'activité de faire ce qu'elle doit faire. Si l'un de vos objets se trouve avoir une référence à une activité, son contexte ou l'une de ses vues lorsqu'il est détruit, vous venez de divulguer cette activité et toute son arborescence de vues.

Solutions: activités et vues

  • Évitez à tout prix de faire une référence statique à une vue ou une activité.
  • Toutes les références aux contextes d'activité doivent être de courte durée (la durée de la fonction)
  • Si vous avez besoin d'un contexte de longue durée, utilisez le contexte d'application ( getBaseContext()ou getApplicationContext()). Ceux-ci ne conservent pas implicitement les références.
  • Vous pouvez également limiter la destruction d'une activité en remplaçant les modifications de configuration. Cependant, cela n'empêche pas d'autres événements potentiels de détruire l'activité. Bien que vous puissiez le faire, vous pouvez toujours vous référer aux pratiques ci-dessus.

Runnables: Introduction

Les runnables ne sont en fait pas si mal. Je veux dire, ils pourraient l' être, mais vraiment nous avons déjà touché la plupart des zones de danger. Une Runnable est une opération asynchrone qui exécute une tâche indépendante du thread sur lequel elle a été créée. La plupart des exécutables sont instanciés à partir du thread d'interface utilisateur. En substance, l'utilisation d'un Runnable crée un autre thread, juste un peu plus géré. Si vous classe un Runnable comme une classe standard et suivez les instructions ci-dessus, vous devriez rencontrer quelques problèmes. La réalité est que de nombreux développeurs ne le font pas.

Par facilité, lisibilité et flux de programme logique, de nombreux développeurs utilisent des classes internes anonymes pour définir leurs Runnables, comme l'exemple que vous créez ci-dessus. Cela donne un exemple comme celui que vous avez tapé ci-dessus. Une classe intérieure anonyme est fondamentalement une classe intérieure discrète. Vous n'avez tout simplement pas à créer une toute nouvelle définition et simplement remplacer les méthodes appropriées. À tous les autres égards, il s'agit d'une classe interne, ce qui signifie qu'elle conserve une référence implicite à son conteneur.

Runnables et activités / vues

Yay! Cette section peut être courte! En raison du fait que les Runnables s'exécutent en dehors du thread actuel, le danger avec celles-ci vient des opérations asynchrones de longue durée. Si le fichier exécutable est défini dans une activité ou une vue comme une classe interne anonyme OU une classe interne imbriquée, il existe des dangers très graves. En effet, comme indiqué précédemment, il doit savoir qui est son conteneur. Entrez le changement d'orientation (ou la suppression du système). Reportez-vous maintenant aux sections précédentes pour comprendre ce qui vient de se passer. Oui, votre exemple est assez dangereux.

Solutions: Runnables

  • Essayez d'étendre Runnable, s'il ne casse pas la logique de votre code.
  • Faites de votre mieux pour rendre les Runnables étendus statiques, s'ils doivent être des classes imbriquées.
  • Si vous devez utiliser des Runnables anonymes, évitez de les créer dans un objet qui a une référence longue durée à une activité ou une vue en cours d'utilisation.
  • De nombreux Runnables auraient tout aussi bien pu être des AsyncTasks. Pensez à utiliser AsyncTask car ceux-ci sont gérés par VM par défaut.

Répondre à la dernière question maintenant pour répondre aux questions qui n'ont pas été directement abordées par les autres sections de cet article. Vous avez demandé "Quand un objet d'une classe interne peut-il survivre plus longtemps que sa classe externe?" Avant d'en arriver là, permettez-moi de le souligner à nouveau: bien que vous ayez raison de vous en préoccuper dans Activités, cela peut provoquer une fuite n'importe où. Je vais fournir un exemple simple (sans utiliser d'activité) juste pour démontrer.

Voici un exemple courant d'une usine de base (sans le code).

public class LeakFactory
{//Just so that we have some data to leak
    int myID = 0;
// Necessary because our Leak class is an Inner class
    public Leak createLeak()
    {
        return new Leak();
    }

// Mass Manufactured Leak class
    public class Leak
    {//Again for a little data.
       int size = 1;
    }
}

Ce n'est pas un exemple aussi courant, mais assez simple à démontrer. La clé ici est le constructeur ...

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Gotta have a Factory to make my holes
        LeakFactory _holeDriller = new LeakFactory()
    // Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//Store them in the class member
            myHoles[i] = _holeDriller.createLeak();
        }

    // Yay! We're done! 

    // Buh-bye LeakFactory. I don't need you anymore...
    }
}

Maintenant, nous avons des fuites, mais pas d'usine. Même si nous avons sorti Factory, il restera en mémoire car chaque fuite a une référence. Peu importe que la classe externe ne dispose pas de données. Cela se produit beaucoup plus souvent qu'on ne pourrait le penser. Nous n'avons pas besoin du créateur, juste de ses créations. Nous en créons donc un temporairement, mais utilisons les créations indéfiniment.

Imaginez ce qui se passe lorsque nous changeons légèrement le constructeur.

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//WOW! I don't even have to create a Factory... 
        // This is SOOOO much prettier....
            myHoles[i] = new LeakFactory().createLeak();
        }
    }
}

Maintenant, chacun de ces nouveaux LeakFactories vient d'être divulgué. Que penses-tu de cela? Ce sont deux exemples très courants de la façon dont une classe interne peut survivre à une classe externe de n'importe quel type. Si cette classe externe avait été une activité, imaginez à quel point cela aurait été pire.

Conclusion

Ceux-ci répertorient les principaux dangers connus de l'utilisation inappropriée de ces objets. En général, ce message aurait dû couvrir la plupart de vos questions, mais je comprends qu'il s'agissait d'un long message, donc si vous avez besoin de clarifications, faites le moi savoir. Tant que vous suivrez les pratiques ci-dessus, vous aurez très peu de soucis de fuite.


3
Merci beaucoup pour cette réponse claire et détaillée. Je ne comprends tout simplement pas ce que vous entendez par «de nombreux développeurs utilisent des fermetures pour définir leurs Runnables»
Sébastien

1
Les fermetures en Java sont des classes internes anonymes, comme le Runnable que vous décrivez. C'est un moyen d'utiliser une classe (presque l'étendre) sans écrire une classe définie qui étend Runnable. C'est ce qu'on appelle une fermeture parce que c'est "une définition de classe fermée" en ce sens qu'elle a son propre espace mémoire fermé dans l'objet contenant réel.
Fuzzical Logic

26
Rédaction éclairante! Une remarque concernant la terminologie: il n'existe pas de classe interne statique en Java. ( Documents ). Une classe imbriquée est statique ou interne , mais ne peut pas être les deux en même temps.
jenzz

2
Bien que cela soit techniquement correct, Java vous permet de définir des classes statiques à l'intérieur des classes statiques. La terminologie n'est pas à mon avantage, mais à l'avantage d'autres qui ne comprennent pas la sémantique technique. C'est pourquoi il est mentionné pour la première fois qu'ils sont de "haut niveau". Les documents pour les développeurs Android utilisent également cette terminologie, et c'est pour les personnes qui s'intéressent au développement Android, j'ai donc pensé qu'il était préférable de garder la cohérence.
Fuzzical Logic

13
Excellent article, l'un des meilleurs de StackOverflow, en particulier pour Android.
StackOverflowed
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.