Aplicación de Android sin problemas de memoria: probé todo y aún no tengo

87

Pasé 4 días completos intentando todo lo posible para descubrir la pérdida de memoria en una aplicación que estoy desarrollando, pero las cosas dejaron de tener sentido hace mucho tiempo.

La aplicación que estoy desarrollando es de naturaleza social, así que piense en Actividades de perfil (P) y enumere Actividades con datos, por ejemplo, insignias (B). Puede saltar de un perfil a una lista de insignias, a otros perfiles, a otras listas, etc.

Así que imagina un flujo como este P1 -> B1 -> P2 -> B2 -> P3 -> B3, etc. Por coherencia, estoy cargando perfiles e insignias del mismo usuario, por lo que cada página P es igual y también lo es cada página B

La esencia general del problema es: después de navegar un poco, dependiendo del tamaño de cada página, obtengo una excepción de memoria insuficiente en lugares aleatorios (mapas de bits, cadenas, etc.), no parece ser coherente.

Después de hacer todo lo imaginable para averiguar por qué me estoy quedando sin memoria, no he encontrado nada. Lo que no entiendo es por qué Android no está matando P1, B1, etc. si se queda sin memoria al cargarse y, en cambio, se bloquea. Esperaría que estas actividades anteriores mueran y resuciten si alguna vez vuelvo a ellas a través de onCreate () y onRestoreInstanceState ().

Y mucho menos esto, incluso si hago P1 -> B1 -> Atrás -> B1 -> Atrás -> B1, todavía tengo un bloqueo. Esto indica algún tipo de pérdida de memoria, sin embargo, incluso después de descargar hprof y usar MAT y JProfiler, no puedo identificarlo.

Deshabilité la carga de imágenes desde la web (y aumenté los datos de prueba cargados para compensarlo y hacer que la prueba sea justa) y me aseguré de que la caché de imágenes usa SoftReferences. Android en realidad intenta liberar las pocas SoftReferences que tiene, pero justo antes de que se quede sin memoria.

Las páginas de insignias obtienen datos de la web, los cargan en una matriz de EntityData desde un BaseAdapter y los envían a ListView (en realidad estoy usando el excelente MergeAdapter de CommonsWare , pero en esta actividad de Insignias, en realidad solo hay 1 adaptador de todos modos, pero yo quería mencionar este hecho de cualquier manera).

Revisé el código y no pude encontrar nada que pudiera filtrarse. Borré y anulé todo lo que pude encontrar e incluso System.gc () a la izquierda y a la derecha, pero la aplicación aún se bloquea.

Todavía no entiendo por qué las actividades inactivas que están en la pila no se cosechan, y realmente me encantaría averiguarlo.

En este punto, estoy buscando sugerencias, consejos, soluciones ... cualquier cosa que pueda ayudar.

Gracias.

Artem Russakovskii
fuente
Entonces, si abrí 15 actividades en los últimos 20 segundos (el usuario está pasando muy rápido), ¿podría ser ese el problema? ¿Qué línea de código debo agregar para borrar la actividad después de que se haya mostrado? Me sale un outOfMemoryerror. ¡Gracias!
Ruchir Baronia

Respuestas:

109

Todavía no entiendo por qué las actividades inactivas que están en la pila no se cosechan, y realmente me encantaría averiguarlo.

No es así como funcionan las cosas. La única administración de memoria que afecta el ciclo de vida de la actividad es la memoria global en todos los procesos, ya que Android decide que se está quedando sin memoria y, por lo tanto, debe eliminar los procesos en segundo plano para recuperar algo.

Si su aplicación está en primer plano iniciando más y más actividades, nunca pasará a segundo plano, por lo que siempre alcanzará su límite de memoria de proceso local antes de que el sistema se acerque a matar su proceso. (Y cuando mata su proceso, matará el proceso que aloja todas las actividades, incluyendo lo que esté actualmente en primer plano).

Entonces, me parece que su problema básico es: está dejando que se ejecuten demasiadas actividades al mismo tiempo y / o cada una de esas actividades se aferra a demasiados recursos.

Solo necesita rediseñar su navegación para no depender de acumular una cantidad arbitraria de actividades potencialmente pesadas. A menos que haga una gran cantidad de cosas en onStop () (como llamar a setContentView () para borrar la jerarquía de vista de la actividad y borrar las variables de cualquier otra cosa a la que pueda estar aferrándose), simplemente se quedará sin memoria.

Es posible que desee considerar el uso de las nuevas API de Fragment para reemplazar esta pila arbitraria de actividades con una sola actividad que administra su memoria de manera más estricta. Por ejemplo, si usa las instalaciones de la pila posterior de fragmentos, cuando un fragmento va a la pila trasera y ya no es visible, se llama a su método onDestroyView () para eliminar por completo su jerarquía de vistas, reduciendo en gran medida su huella.

Ahora, en la medida en que choques en el flujo donde presionas hacia atrás, vas a una actividad, presionas hacia atrás, vas a otra actividad, etc. y nunca tienes una pila profunda, entonces sí, solo tienes una fuga. Esta publicación de blog describe cómo depurar fugas: http://android-developers.blogspot.com/2011/03/memory-analysis-for-android.html

hackbod
fuente
47
En defensa del OP, esto no es lo que sugiere la documentación. Citando developer.android.com/guide/topics/fundamentals/… "(Cuando se detiene una actividad), ya no es visible para el usuario y el sistema puede eliminarla cuando se necesita memoria en otro lugar". "Si una actividad está pausada o detenida, el sistema puede eliminarla de la memoria ... pidiéndole que termine (llamando a su método finish ())" "(se llama a onDestroy ()) porque el sistema está destruyendo temporalmente esta instancia de la actividad para ahorrar espacio ".
CommonsWare
42
En la misma página, "Sin embargo, cuando el sistema destruye una actividad para recuperar memoria". Lo que está indicando es que Android nunca destruye actividades para recuperar memoria, sino que solo terminará el proceso para hacerlo. Si es así, esta página necesita una reescritura seria, ya que sugiere repetidamente que Android destruirá una actividad para recuperar memoria. También tenga en cuenta que muchos de los pasajes citados también existen en ActivityJavaDocs.
CommonsWare
6
Pensé que había puesto esto aquí, pero publiqué
Justin Breitfeller
15
Es 2013 y los documentos solo han llegado a presentar el punto falso de manera más clara: developer.android.com/training/basics/activity-lifecycle/… , "Una vez que su actividad se detiene, el sistema podría destruir la instancia si necesita recuperarse memoria del sistema. En casos EXTREMOS, el sistema podría simplemente
interrumpir el
2
Bueno, mierda. Definitivamente siempre pensé (debido a los documentos) que el sistema administraba las actividades detenidas en su proceso y las serializaría y deserializaría (destruiría y crearía) según fuera necesario.
dcow
21

Algunos consejos:

  1. Asegúrese de no filtrar el contexto de la actividad.

  2. Asegúrese de no guardar referencias en mapas de bits. Limpie todos sus ImageView en Activity # onStop, algo como esto:

    Drawable d = imageView.getDrawable();  
    if (d != null) d.setCallback(null);  
    imageView.setImageDrawable(null);  
    imageView.setBackgroundDrawable(null);
    
  3. Recicle los mapas de bits si ya no los necesita.

  4. Si usa memoria caché, como memory-lru, asegúrese de que no esté usando demasiada memoria.

  5. No solo las imágenes ocupan mucha memoria, asegúrese de no guardar demasiados datos en la memoria. Esto puede suceder fácilmente si tiene listas infinitas en su aplicación. Intente almacenar en caché los datos en DataBase.

  6. En Android 4.2, hay un error (stackoverflow # 13754876) con la aceleración de hardware, por lo que si lo usa hardwareAccelerated=trueen su manifiesto, perderá memoria. GLES20DisplayList- siga guardando referencias, incluso si hizo el paso (2) y nadie más está haciendo referencia a este mapa de bits. Aquí necesitas:

    a) deshabilitar la aceleración de hardware para api 16/17;
    o
    b) separar la vista que contiene el mapa de bits

  7. Para Android 3+, puede intentar usarlo android:largeHeap="true"en su AndroidManifest. Pero no resolverá tus problemas de memoria, simplemente posponlos.

  8. Si necesita, como, navegación infinita, entonces Fragmentos - debería ser su elección. Entonces tendrás 1 actividad, que simplemente cambiará entre fragmentos. De esta forma también resolverás algunos problemas de memoria, como el número 4.

  9. Utilice Memory Analyzer para averiguar la causa de la pérdida de memoria.
    Aquí hay un muy buen video de Google I / O 2011: Administración de memoria para aplicaciones de Android
    Si se trata de mapas de bits, debe leer esto: Visualización de mapas de bits de manera eficiente

vovkab
fuente
Entonces, si abrí 15 actividades en los últimos 20 segundos (el usuario está pasando muy rápido), ¿podría ser ese el problema? ¿Qué línea de código debo agregar para borrar la actividad después de que se haya mostrado? Me sale un outOfMemoryerror. ¡Gracias!
Ruchir Baronia
4

Los mapas de bits suelen ser los culpables de los errores de memoria en Android, por lo que sería una buena área para verificar.

Ed Burnette
fuente
Entonces, si abrí 15 actividades en los últimos 20 segundos (el usuario está pasando muy rápido), ¿podría ser ese el problema? ¿Qué línea de código debo agregar para borrar la actividad después de que se haya mostrado? Me sale un outOfMemoryerror. ¡Gracias!
Ruchir Baronia
2

¿Tiene algunas referencias a cada actividad? AFAIK, esta es una razón que evita que Android elimine actividades de la pila.

¿También podemos reproducir este error en otros dispositivos? He experimentado un comportamiento extraño de algunos dispositivos Android según la ROM y / o el fabricante del hardware.

NewProggie
fuente
Pude reproducir esto en un Droid que ejecuta CM7 con un montón máximo establecido en 16 MB, que es el mismo valor que estoy probando en el emulador.
Artem Russakovskii
Puede estar en algo. Cuando comience la segunda actividad, ¿la primera lo hará onPause-> onStop o simplemente onPause? Porque estoy imprimiendo todo en llamadas de ciclo de vida *******, y veo onPause -> onCreate, sin onStop. Y uno de los volcados de emergencia dijo algo como onPause = true o onStop = false para como 3 actividades que estaba matando.
Artem Russakovskii
Se debe llamar a OnStop cuando la actividad abandona la pantalla, pero puede que no sea así si el sistema la recupera antes.
Dten
No se reclama porque no veo onCreate y onRestoreInstanceState llamado si hago clic en Atrás.
Artem Russakovskii
Sin embargo, de acuerdo con el ciclo de vida, es posible que nunca se llamen, verifique mi respuesta que tiene un enlace al blog oficial del desarrollador, es más que probable que sea la forma en que está pasando sus mapas de bits
dten
2

Creo que el problema quizás sea una combinación de muchos factores indicados aquí en las respuestas, lo que le está dando problemas. Como dijo @Tim, una referencia (estática) a una actividad o un elemento en esa actividad puede hacer que el GC omita la Actividad. Aquí está el artículo que discute esta faceta. Creo que el problema probable proviene de algo que mantiene la Actividad en un estado de "Proceso visible" o superior, lo que prácticamente garantizará que la Actividad y sus recursos asociados nunca se recuperen.

Pasé por el problema opuesto hace un tiempo con un Servicio, así que eso es lo que me hizo pensar: hay algo que mantiene su Actividad en la parte superior de la lista de prioridades del proceso para que no esté sujeta al sistema GC, como una referencia (@Tim) o un bucle (@Alvaro). El bucle no tiene por qué ser un elemento de ejecución prolongada o sin fin, solo algo que se parece mucho a un método recursivo o un bucle en cascada (o algo por el estilo).

EDITAR: Según tengo entendido, Android llama automáticamente a onPause y onStop según sea necesario. Los métodos están ahí principalmente para que los modifique para que pueda ocuparse de lo que necesita antes de que se detenga el proceso de alojamiento (guardar variables, guardar manualmente el estado, etc.); pero tenga en cuenta que se indica claramente que onStop (junto con onDestroy) no se puede llamar en todos los casos. Además, si el proceso de alojamiento también aloja una Actividad, Servicio, etc. que tiene un estado "Forground" o "Visible", es posible que el sistema operativo ni siquiera busque detener el proceso / hilo. Por ejemplo: una Actividad y un Servicio se combinan en el mismo proceso y el Servicio regresaSTART_STICKY deonStartCommand()el proceso toma automáticamente al menos un estado visible. Esa podría ser la clave aquí, intente declarar un nuevo proceso para la Actividad y vea si eso cambia algo. Intente agregar esta línea a la declaración de su Actividad en el Manifiesto como: android:process=":proc2"y luego ejecute las pruebas nuevamente si su Actividad comparte un proceso con cualquier otra cosa. La idea aquí es que si ha limpiado su Actividad y está bastante seguro de que el problema no es su Actividad, entonces el problema es otra cosa y es hora de buscar eso.

Además, no puedo recordar dónde lo vi (si es que lo vi en los documentos de Android) pero recuerdo algo sobre una PendingIntentreferencia a una Actividad que puede hacer que una Actividad se comporte de esta manera.

Aquí hay un enlace a la onStartCommand()página con algunas ideas sobre el proceso de no matar.

Kingsolmn
fuente
Según el enlace que proporcionó (gracias), una actividad se puede eliminar si se ha llamado a su onStop, pero en mi caso, creo que algo impide que onStop se ejecute. Definitivamente investigaré por qué. Sin embargo, también dice que las actividades con solo onPause también se pueden eliminar, lo que no sucede en mi caso (veo que se llama a onPause pero no a onStop).
Artem Russakovskii
1

así que lo único que realmente puedo pensar es si tiene una variable estática que hace referencia directa o indirectamente al contexto. Incluso algo tanto como una referencia a parte de la aplicación. Estoy seguro de que ya lo ha probado, pero lo sugeriré por si acaso, intente anular TODAS sus variables estáticas en onDestroy () solo para asegurarse de que el recolector de basura lo obtenga

Tim Fenton - VenumX
fuente
1

La mayor fuente de pérdida de memoria que he encontrado fue causada por alguna referencia global, de alto nivel o de larga data al contexto. Si mantiene el "contexto" almacenado en una variable en cualquier lugar, puede encontrar pérdidas de memoria impredecibles.

D2TheC
fuente
Sí, escuché y leí esto muchas veces, pero tuve problemas para precisarlo. Si tan solo pudiera descubrir de manera confiable cómo rastrearlos.
Artem Russakovskii
1
En mi caso, esto se tradujo en cualquier variable a nivel de clase que se estableciera igual al contexto (por ejemplo, Class1.variable = getContext ();). En general, reemplazar cada uso de "contexto" en mi aplicación con una nueva llamada a "getContext" o similar resolvió mis mayores problemas de memoria. Pero los míos eran inestables y erráticos, no predecibles como en tu caso, así que posiblemente algo diferente.
D2TheC
1

Intente pasar getApplicationContext () a cualquier cosa que necesite un contexto. Es posible que tenga una variable global que contenga una referencia a sus actividades y evite que se recolecten como basura.

uve
fuente
1

Una de las cosas que realmente ayudó con el problema de la memoria en mi caso terminó siendo la configuración de Purgeable en verdadero para mis Bitmaps. Consulte ¿Por qué NO usaría la opción inPurgeable de BitmapFactory? y la discusión de la respuesta para obtener más información.

La respuesta de Dianne Hackborn y nuestra discusión posterior (también gracias, CommonsWare) ayudaron a aclarar ciertas cosas sobre las que estaba confundido, así que gracias por eso.

Artem Russakovskii
fuente
Sé que este es un tema antiguo, pero veo que parece encontrar este hilo en muchas búsquedas. Quiero vincular un excelente tutorial del que aprendí mucho y que puede encontrar aquí: developer.android.com/training/displaying-bitmaps/index.html
Codeversed
0

Encontré el mismo problema contigo. Estaba trabajando en una aplicación de mensajería instantánea, para el mismo contacto, es posible iniciar un ProfileActivity en un ChatActivity, y viceversa. Solo agrego una cadena adicional en la intención de iniciar otra actividad, toma la información del tipo de clase de actividad de inicio y la identificación del usuario. Por ejemplo, ProfileActivity inicia una ChatActivity, luego en ChatActivity.onCreate, marco el tipo de clase de invocador 'ProfileActivity' y la identificación del usuario, si va a iniciar una Activity, verificaría si es una 'ProfileActivity' para el usuario o no . Si es así, simplemente llame a 'finish ()' y regrese a la ProfileActivity anterior en lugar de crear una nueva. La pérdida de memoria es otra cosa.

qiuping345
fuente