Problèmes de mémoire de l'application Android - tout essayé et toujours perdu

87

J'ai passé 4 jours entiers à essayer tout ce que je pouvais pour trouver 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 au profil Activités (P) et répertoriez les activités avec des données - par exemple des badges (B). Vous pouvez passer d'un profil à une liste de badges, à d'autres profils, à d'autres listes, etc.

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

L'essentiel du problème est le suivant: après avoir navigué un peu, en fonction de la taille de chaque page, j'obtiens une exception de mémoire insuffisante à des endroits aléatoires - Bitmaps, Strings, etc. - cela ne semble pas 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 reviens à elles via onCreate () et onRestoreInstanceState ().

Sans parler de cela - même si je fais P1 -> B1 -> Back -> B1 -> Back -> 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 des images à partir du Web (et augmenté les données de test chargées pour compenser et rendre le test équitable) et me suis assuré que le cache d'images utilise SoftReferences. Android essaie en fait de libérer les quelques SoftReferences dont il dispose, mais juste avant de perdre la mémoire.

Les pages de badges obtiennent des données du Web, les chargent dans un tableau d'EntityData à partir d'un BaseAdapter et les alimentent dans un ListView (j'utilise en fait l' excellent MergeAdapter de CommonsWare , mais dans cette activité de Badge, il n'y a vraiment qu'un seul adaptateur de toute façon, mais je voulait mentionner ce fait de toute façon).

J'ai parcouru le code et je n'ai rien trouvé 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 cherche des indices, des conseils, des solutions ... tout ce qui pourrait aider.

Je vous remercie.

Artem Russakovskii
la source
Donc, si j'ai ouvert 15 activités au cours des 20 dernières secondes (l'utilisateur passe très vite), cela pourrait-il être le problème? Quelle ligne de code dois-je ajouter pour effacer l'activité après son affichage? J'obtiens une outOfMemoryerreur. Merci!
Ruchir Baronia le

Réponses:

109

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 sur tous les processus, car Android décide qu'il manque de mémoire et doit donc tuer les processus d'arrière-plan pour en récupérer.

Si votre application est assise au premier plan et commence de plus en plus d'activités, elle ne passe jamais en arrière-plan, elle atteindra donc 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 tuera le processus hébergeant toutes les activités, y compris 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 se dérouler en même temps et / ou chacune de ces activités consomme trop de ressources.

Il vous suffit 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 des vues de l'activité et effacer les variables de tout ce à quoi elle pourrait s'accrocher), vous allez simplement manquer de mémoire.

Vous voudrez peut-être envisager d'utiliser les nouvelles API 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 la pile arrière des 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, réduisant considérablement son encombrement.

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

hackbod
la source
47
Pour la défense du PO, ce n'est pas ce que la documentation suggère. Citant developer.android.com/guide/topics/fundamentals/… "(Lorsqu'une activité est arrêtée), elle n'est plus visible pour l'utilisateur et elle peut être supprimée par le système lorsque de la mémoire est nécessaire ailleurs." "Si une activité est mise en pause ou arrêtée, le système peut la supprimer de la mémoire ... en lui demandant de terminer (appelant sa méthode finish ())" "(onDestroy () est appelé) car le système détruit temporairement cette instance de l'activité pour économiser de l'espace. "
CommonsWare
42
Sur la même page, "Cependant, lorsque le système détruit une activité pour récupérer de la mémoire". Ce que vous indiquez, c'est qu'Android ne détruit jamais les activités pour récupérer de la mémoire, mais mettra uniquement fin au processus pour le faire. Si c'est le cas, cette page nécessite une réécriture sérieuse, car elle suggère à plusieurs reprises qu'Android détruira une activité pour récupérer de la mémoire. Notez également que de nombreux passages cités existent également dans les ActivityJavaDocs.
CommonsWare
6
Je pensais l'avoir mis ici, mais j'ai posté une demande de mise à jour de la documentation à ce sujet: code.google.com/p/android/issues/detail?id=21552
Justin Breitfeller
15
Nous sommes en 2013 et la documentation n'est venue que pour présenter le faux point plus clairement: developer.android.com/training/basics/activity-lifecycle/… , "Une fois votre activité arrêtée, le système pourrait détruire l'instance s'il a besoin de récupérer mémoire système. Dans les cas EXTRÊMES, le système peut simplement tuer le processus de votre application "
kaay
2
Eh bien, merde. J'ai toujours pensé (à cause de la documentation) que le système gérait les activités arrêtées dans votre processus et les sérialiserait et désérialiserait (détruirait et créerait) selon les besoins.
dcow
21

Quelques conseils:

  1. Assurez-vous que vous n'êtes pas une fuite de contexte d'activité.

  2. Assurez-vous de ne pas conserver 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 un 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, mais assurez-vous de ne pas conserver 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 un bogue (stackoverflow # 13754876) avec l'accélération matérielle, donc si vous utilisez hardwareAccelerated=truedans votre manifeste, il y aura une fuite de mémoire. GLES20DisplayList- gardez 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 de:

    a) désactiver l'accélération matérielle pour l'API 16/17;
    ou
    b) détacher la vue contenant 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 suffit de les reporter.

  8. Si vous avez besoin, comme, d'une navigation infinie, alors Fragments - devrait être votre choix. Vous aurez donc 1 activité, qui basculera simplement entre les fragments. De cette façon, vous résoudrez également 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 les applications Android
    Si vous traitant de bitmaps cela devrait être un incontournable: Affichage bitmaps Efficacement

vovkab
la source
Donc, si j'ai ouvert 15 activités au cours des 20 dernières secondes (l'utilisateur passe très vite), cela pourrait-il être le problème? Quelle ligne de code dois-je ajouter pour effacer l'activité après son affichage? J'obtiens une outOfMemoryerreur. Merci!
Ruchir Baronia le
4

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

Ed Burnette
la source
Donc, si j'ai ouvert 15 activités au cours des 20 dernières secondes (l'utilisateur passe très vite), cela pourrait-il être le problème? Quelle ligne de code dois-je ajouter pour effacer l'activité après son affichage? J'obtiens une outOfMemoryerreur. Merci!
Ruchir Baronia le
2

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

Êtes-vous en mesure de reproduire cette erreur sur d'autres appareils également? J'ai rencontré un comportement étrange de certains appareils Android en fonction de la ROM et / ou du fabricant du matériel.

NouveauProggie
la source
J'ai pu reproduire cela sur un Droid exécutant CM7 avec un tas maximum réglé à 16 Mo, ce qui est la même valeur que je teste sur l'émulateur.
Artem Russakovskii
Vous êtes peut-être sur quelque chose. Lorsque la 2ème activité commencera, la 1ère fera-t-elle onPause-> onStop ou juste onPause? Parce que j'imprime tout sur les appels de cycle de vie *******, et que je vois onPause -> onCreate, sans onStop. Et l'un des vidages sur incident a en fait dit quelque chose comme onPause = true ou onStop = false pour 3 activités similaires qu'il tuait.
Artem Russakovskii
OnStop doit être appelé lorsque l'activité quitte l'écran, mais peut ne pas l'être si elle est récupérée tôt par le système.
dten le
Il n'est pas récupéré car je ne vois pas onCreate et onRestoreInstanceState appelés si je clique en arrière.
Artem Russakovskii
selon le cycle de vie, ceux-ci pourraient ne jamais être appelés, vérifiez ma réponse qui a un lien vers le blog officiel des développeurs, c'est plus que probablement la façon dont vous transmettez vos bitmaps
dten
2

Je pense que le problème est peut-être une combinaison de nombreux facteurs énoncés ici dans les réponses qui vous posent des problèmes. Comme @Tim l'a dit, une référence (statique) à une activité ou à un élément de cette activité peut amener le GC à ignorer l'activité. Voici 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 plus, 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 inverse 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 système GC, comme une référence (@Tim) ou une boucle (@Alvaro). La boucle n'a pas besoin d'être un élément sans fin ou long, juste quelque chose qui fonctionne beaucoup comme une méthode récursive ou une boucle en cascade (ou quelque chose du genre).

EDIT: Si je comprends bien, onPause et onStop sont appelés automatiquement selon les besoins par Android. Les méthodes sont là principalement pour que vous puissiez les remplacer afin que vous puissiez prendre soin de ce dont vous avez besoin avant que le processus d'hébergement ne soit arrêté (sauvegarde des variables, sauvegarde manuelle de l'état, etc.); mais notez qu'il est clairement indiqué que onStop (avec onDestroy) peut ne pas être 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 deux ancrés dans le même processus et le service revient START_STICKYdeonStartCommand()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 en tant que: android:process=":proc2"puis réexécutez 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 à peu près 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 chercher cela.

De plus, je ne me souviens pas où je l'ai vu (si je l'ai même vu dans la documentation Android), mais je me souviens que quelque chose à propos du PendingIntentréférencement d'une activité peut entraîner le comportement d'une activité de cette façon.

Voici un lien vers la onStartCommand()page avec quelques informations sur le processus de non-meurtre.

Kingsolmn
la source
Sur la base du lien que vous avez fourni (merci), une activité peut être tuée si son onStop a été appelé, mais dans mon cas, je pense que quelque chose empêche onStop de fonctionner. Je vais certainement chercher pourquoi. Cependant, il indique également que les activités avec seulement onPause peuvent également être tuées, ce qui ne se produit pas dans mon cas (je vois onPause être appelé mais pas onStop).
Artem Russakovskii
1

donc la seule chose à laquelle je peux vraiment penser est si vous avez une variable statique qui fait référence directement ou indirectement au contexte. Même quelque chose comme une référence à une partie de l'application. 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

Tim Fenton - VenumX
la source
1

La plus grande source de fuite de mémoire que j'ai trouvée était 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.

Dirk Conrad Coetsee
la source
Oui, j'ai entendu et lu ceci plusieurs fois, mais j'ai eu du mal à le cerner. Si seulement je pouvais trouver de manière fiable comment les retracer.
Artem Russakovskii
1
Dans mon cas, cela s'est traduit par n'importe quelle variable au niveau de la classe qui était définie comme étant égale au contexte (par exemple, Class1.variable = getContext ();). En général, le remplacement de chaque utilisation de "context" dans mon application par un nouvel appel à "getContext" ou similaire a résolu mes plus gros problèmes de mémoire. Mais les miens étaient instables et erratiques, pas prévisibles comme dans votre cas, donc peut-être quelque chose de différent.
Dirk Conrad Coetsee
1

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

vee
la source
1

L'une des choses qui a vraiment aidé le problème de mémoire dans mon cas a fini par être de définir inPurgeable sur true pour mes Bitmaps. Voir Pourquoi est-ce que je n'utiliserais jamais 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 ultérieure (également merci, CommonsWare) ont aidé à clarifier certaines choses sur lesquelles j'étais confuse, alors merci pour cela.

Artem Russakovskii
la source
Je sais que c'est un vieux sujet mais je vois semble trouver ce fil sur de nombreuses recherches. Je veux relier un excellent tutoriel que j'ai tellement appris que vous pouvez trouver ici: developer.android.com/training/displaying-bitmaps/index.html
Codeversed
0

J'ai rencontré le même problème avec vous. Je travaillais sur une application de messagerie instantanée, pour le même contact, il est possible de démarrer un ProfileActivity dans un ChatActivity, et vice versa. J'ajoute simplement 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 un ChatActivity, puis dans ChatActivity.onCreate, je marque le type de classe invocateur 'ProfileActivity' et l'ID utilisateur, s'il va démarrer une activité, je vérifierais s'il s'agit d'un '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.

qiuping345
la source