web-dev-qa-db-fra.com

Android app problèmes de mémoire insuffisante - tout essayé et toujours à perte

J'ai passé 4 jours complets à essayer tout ce que je pouvais pour comprendre la fuite de mémoire dans une application que je développais, mais les choses ont cessé de faire sens il y a longtemps.

L'application que je développe est de nature sociale, alors pensez à profiler les activités (P) et à lister les activités avec des données - par exemple les badges (B). Vous pouvez passer d'un profil à une liste de badges à d'autres profils, à d'autres listes, etc.

Imaginez donc un flux comme celui-ci P1 -> B1 -> P2 -> B2 -> P3 -> B3, etc. Par souci de cohérence, je charge des profils et des badges du même utilisateur, de sorte que chaque page P est la même et ainsi chaque page B.

L'essentiel du problème est le suivant: après avoir navigué un peu, selon la taille de chaque page, j'obtiens une exception de mémoire insuffisante à des endroits aléatoires - Bitmaps, chaînes, etc. - cela ne semble pas être cohérent.

Après avoir fait tout ce qui est imaginable pour comprendre pourquoi je manque de mémoire, je n'ai rien trouvé. Ce que je ne comprends pas, c'est pourquoi Android ne tue pas P1, B1, etc. s'il manque de mémoire lors du chargement et se bloque à la place. Je m'attendrais à ce que ces activités antérieures meurent et soient ressuscitées si jamais je leur retourne via onCreate () et onRestoreInstanceState ().

Sans parler de cela - même si je fais P1 -> B1 -> Retour -> B1 -> Retour -> B1, je reçois toujours un crash. Cela indique une sorte de fuite de mémoire, mais même après avoir vidé hprof et utilisé MAT et JProfiler, je ne peux pas le localiser.

J'ai désactivé le chargement d'images à partir du Web (et augmenté les données de test chargées pour compenser cela et rendre le test équitable) et m'assurer que le cache d'image utilise SoftReferences. Android essaie en fait de libérer les quelques références logicielles qu'il possède, mais juste avant qu'il ne tombe en panne de mémoire.

Les pages de badges récupèrent les données du Web, les chargent dans un tableau d'EntityData à partir d'un BaseAdapter et les alimentent vers un ListView (j'utilise en fait CommonsWare excellent MergeAdapter , mais dans cette activité de Badge, il y a vraiment seulement 1 adaptateur de toute façon, mais je voulais mentionner ce fait de toute façon).

J'ai parcouru le code et je n'ai pas pu trouver quoi que ce soit qui fuirait. J'ai effacé et annulé tout ce que je pouvais trouver et même System.gc () à gauche et à droite, mais l'application se bloque toujours.

Je ne comprends toujours pas pourquoi les activités inactives qui sont sur la pile ne sont pas récoltées, et j'aimerais vraiment comprendre cela.

À ce stade, je recherche des astuces, des conseils, des solutions ... tout ce qui pourrait aider.

Merci.

87
Artem Russakovskii

L'une des choses qui a vraiment aidé le problème de mémoire dans mon cas a fini par définir inPurgeable sur true pour mes Bitmaps. Voir Pourquoi ne devrais-je jamais utiliser l'option inPurgeable de BitmapFactory? et la discussion de la réponse pour plus d'informations.

La réponse de Dianne Hackborn et notre discussion subséquente (merci également, CommonsWare) ont aidé à clarifier certaines choses qui m'ont troublé, alors merci pour cela.

1
Artem Russakovskii

Je ne comprends toujours pas pourquoi les activités inactives qui sont sur la pile ne sont pas récoltées, et j'aimerais vraiment comprendre cela.

Ce n'est pas ainsi que les choses fonctionnent. La seule gestion de la mémoire qui a un impact sur le cycle de vie de l'activité est la mémoire globale de tous les processus, car Android décide qu'il est en cours d'exécution manque de mémoire et a donc besoin de tuer les processus d'arrière-plan pour en récupérer.

Si votre application se trouve au premier plan et commence de plus en plus d'activités, elle ne passe jamais en arrière-plan, donc elle atteindra toujours sa limite de mémoire de processus locale avant que le système ne soit sur le point de tuer son processus. (Et quand il tue son processus, il tue le processus hébergeant toutes les activités, y compris tout ce qui est actuellement au premier plan.)

Il me semble donc que votre problème de base est le suivant: vous laissez trop d'activités s'exécuter en même temps et/ou chacune de ces activités contient trop de ressources.

Vous avez juste besoin de repenser votre navigation pour ne pas compter sur l'empilement d'un nombre arbitraire d'activités potentiellement lourdes. À moins que vous ne fassiez beaucoup de choses dans onStop () (comme appeler setContentView () pour effacer la hiérarchie de vue de l'activité et effacer les variables de tout ce à quoi il peut s'accrocher), vous allez juste manquer de mémoire.

Vous pouvez envisager d'utiliser les nouvelles API de fragment pour remplacer cette pile arbitraire d'activités par une seule activité qui gère plus étroitement sa mémoire. Par exemple, si vous utilisez les fonctionnalités de pile arrière de fragments, lorsqu'un fragment va sur la pile arrière et n'est plus visible, sa méthode onDestroyView () est appelée pour supprimer complètement sa hiérarchie de vues, ce qui réduit considérablement son encombrement.

Maintenant, dans la mesure où vous vous écrasez dans le flux où vous appuyez, allez à une activité, appuyez à nouveau, allez à une autre activité, etc. et n'avez jamais de pile profonde, alors oui, vous avez juste une fuite. Ce billet de blog décrit comment déboguer les fuites: http://Android-developers.blogspot.com/2011/03/memory-analysis-for-Android.html

106
hackbod

Quelques conseils:

  1. Assurez-vous de ne pas divulguer le contexte d'activité.

  2. Assurez-vous que vous ne conservez pas de références sur les bitmaps. Nettoyez tous vos ImageView dans Activity # onStop, quelque chose comme ceci:

    Drawable d = imageView.getDrawable();  
    if (d != null) d.setCallback(null);  
    imageView.setImageDrawable(null);  
    imageView.setBackgroundDrawable(null);
    
  3. Recyclez les bitmaps si vous n'en avez plus besoin.

  4. Si vous utilisez le cache mémoire, comme memory-lru, assurez-vous qu'il n'utilise pas trop de mémoire.

  5. Non seulement les images prennent beaucoup de mémoire, assurez-vous que vous ne conservez pas trop d'autres données en mémoire. Cela peut facilement se produire si vous avez des listes infinies dans votre application. Essayez de mettre en cache les données dans DataBase.

  6. Sur Android 4.2, il y a bug (stackoverflow # 13754876) avec accélération matérielle, donc si vous utilisez hardwareAccelerated=true dans votre manifeste, il y aura une fuite de mémoire. GLES20DisplayList - continuez à conserver les références, même si vous avez fait l'étape (2) et que personne d'autre ne fait référence à ce bitmap. Ici, vous avez besoin:

    a) désactiver l'accélération matérielle pour api 16/17;
    ou
    b) détacher la vue qui contient le bitmap

  7. Pour Android 3+, vous pouvez essayer d'utiliser Android:largeHeap="true" dans votre AndroidManifest. Mais cela ne résoudra pas vos problèmes de mémoire, il vous suffit de les reporter.

  8. Si vous avez besoin, par exemple, d'une navigation infinie, alors Fragments - devrait être votre choix. Vous aurez donc 1 activité, qui ne fera que basculer entre les fragments. De cette façon, vous pourrez également résoudre certains problèmes de mémoire, comme le numéro 4.

  9. Utilisez Memory Analyzer pour trouver la cause de votre fuite de mémoire.
    Voici une très bonne vidéo de Google I/O 2011: Gestion de la mémoire pour Android Apps
    Si vous traitez avec des bitmaps, ceci devrait être une lecture incontournable: Affichage efficace des bitmaps

21
vovkab

Les bitmaps sont souvent à l'origine des erreurs de mémoire sur Android, ce serait donc un bon point à vérifier.

4
Ed Burnette

Tenez-vous des références à chaque activité? AFAIK c'est une raison qui empêche Android de supprimer les activités de la pile.

Sommes-nous capables de reproduire cette erreur sur d'autres appareils également? J'ai rencontré un comportement étrange de certains appareils Android selon le ROM et/ou le fabricant du matériel).

2
NewProggie

Je pense que le problème peut-être une combinaison de nombreux facteurs énoncés ici dans les réponses est ce qui vous pose des problèmes. Comme l'a dit @Tim, une référence (statique) à une activité ou à un élément de cette activité peut amener le GC à ignorer l'activité. Ici est l'article traitant de cette facette. Je pense que le problème probable vient de quelque chose qui maintient l'activité dans un état "Processus visible" ou supérieur, ce qui garantira à peu près que l'activité et ses ressources associées ne seront jamais récupérées.

J'ai traversé le problème opposé il y a quelque temps avec un service, c'est donc ce qui m'a poussé à réfléchir: il y a quelque chose qui maintient votre activité en haut de la liste des priorités de processus afin qu'elle ne soit pas soumise au GC du système, comme une référence (@Tim) ou une boucle (@Alvaro). La boucle n'a pas besoin d'être un élément sans fin ou de longue durée, juste quelque chose qui fonctionne beaucoup comme une méthode récursive ou une boucle en cascade (ou quelque chose dans ce sens).

EDIT: Si je comprends bien, onPause et onStop sont appelés automatiquement selon les besoins par Android. Les méthodes sont là principalement pour vous afin que vous puissiez prendre soin de ce dont vous avez besoin avant que le processus d'hébergement ne soit arrêté (enregistrement des variables, enregistrement manuel de l'état, etc.); mais notez qu'il est clairement indiqué que onStop (avec onDestroy) peut-être pas sera appelé dans tous les cas. De plus, si le processus d'hébergement héberge également une activité, un service, etc. qui a un statut "Forground" ou "Visible", le système d'exploitation peut même ne pas envisager d'arrêter le processus/thread. Par exemple: une activité et un service sont tous les deux lancés dans le même processus et le service renvoie START_STICKY De onStartCommand() le processus prend automatiquement au moins un statut visible. Cela pourrait être la clé ici, essayez de déclarer un nouveau proc pour l'activité et voyez si cela change quelque chose. Essayez d'ajouter cette ligne à la déclaration de votre activité dans le manifeste sous la forme: Android:process=":proc2", Puis relancez les tests si votre activité partage un processus avec autre chose. L'idée ici est que si vous avez nettoyé votre activité et êtes joli sûr que le problème n'est pas votre activité, alors quelque chose d'autre est le problème et il est temps de chasser pour cela.

De plus, je ne me souviens pas où je l'ai vu (si je l'ai même vu dans les documents Android) mais je me souviens de quelque chose à propos d'un PendingIntentla référence à une activité peut provoquer une activité se comporter de cette façon.

Ici est un lien vers la page onStartCommand() avec quelques informations sur le front non destructif du processus.

2
Kingsolmn

La plus grande source de fuite de mémoire que j'ai trouvée a été causée par une référence globale, de haut niveau ou de longue date au contexte. Si vous conservez le "contexte" stocké dans une variable n'importe où, vous pouvez rencontrer des fuites de mémoire imprévisibles.

1
Dirk Conrad Coetsee

donc la seule chose à laquelle je peux vraiment penser est si vous avez une variable statique qui fait directement ou indirectement référence au contexte. Même quelque chose comme une référence à une partie de la demande. Je suis sûr que vous l'avez déjà essayé, mais je le suggérerai juste au cas où, essayez simplement d'annuler TOUTES vos variables statiques dans onDestroy () juste pour vous assurer que le garbage collector l'obtient

1
Tim Fenton - VenumX

Essayez de passer getApplicationContext () à tout ce qui a besoin d'un contexte. Vous pouvez avoir une variable globale qui contient une référence à vos activités et les empêche d'être récupérées.

1
vee

J'ai rencontré le même problème avec toi. Je travaillais sur une application de messagerie instantanée, pour le même contact, il est possible de démarrer une ProfileActivity dans une ChatActivity, et vice versa. J'ajoute juste une chaîne supplémentaire dans l'intention de démarrer une autre activité, elle prend les informations du type de classe d'activité de démarrage et l'ID utilisateur. Par exemple, ProfileActivity démarre une ChatActivity, puis dans ChatActivity.onCreate, je marque le type de classe invocatrice 'ProfileActivity' et l'ID utilisateur, si cela va démarrer une activité, je vérifierais s'il s'agit d'une 'ProfileActivity' pour l'utilisateur ou non . Si c'est le cas, appelez simplement 'finish ()' et revenez à l'ancien ProfileActivity au lieu d'en créer un nouveau. La fuite de mémoire est une autre chose.

0
qiuping345