Fuga de memoria en Android

Cómo una adecuada gestión del ciclo de vida puede prevenir las fugas de memoria en Android

La excepción OutOfMemoryException es un error común y frustrante, y una de las principales causas de cierre inesperado de la aplicación.

«¿Por qué está sucediendo esto ahora, si la aplicación funcionaba perfectamente ayer?». Es una pregunta que deja perplejos tanto a los desarrolladores novatos como a los avanzados de Android de todo el mundo.

Hay una gran variedad de causas potenciales de OutOfMemory Exceptions, pero una de las más comunes es la fuga de memoria: la asignación de memoria en la aplicación que nunca se libera. Este artículo explicará cómo minimizar este riesgo a través de la gestión eficaz del ciclo de vida – una parte crucial, pero a menudo se pasa por alto del proceso de desarrollo.

TL;DR

En este artículo hablaremos de:

  • Las razones de las fugas de memoria en Android
  • Pasos básicos para prevenirlas
  • Formas más avanzadas de gestión del ciclo de vida
  • Herramientas que pueden gestionar el ciclo de vida de tu producto

¿Por qué se producen fugas de memoria RAM en Android?

El problema es sencillo. Ciertos objetos sólo deberían tener un tiempo de vida fijo, y cuando su vida útil termina, deben ser eliminados.

En teoría, esta memoria debería ser eliminada cuando se mata un proceso usando onStop o onDestroy. Sin embargo, un mal uso de la referenciación de objetos puede impedir que el recolector de basura desocupe los objetos no utilizados. Por ejemplo: si el objeto no utilizado A hace referencia al objeto no utilizado B, se terminará con dos objetos innecesarios que el recolector de basura nunca desocupará ya que se están referenciando el uno al otro.

Trucos comunes para evitar que esto ocurra

Los desarrolladores pueden tomar una serie de medidas para evitar que las actividades muertas queden atrapadas en la memoria.

  1. Registrar/desregistrar BroadcastReceivers en onResume()/onPause() o onStart()/onStop()
  2. No utilizar NUNCA variables estáticas para Views/Activities/Contexts
  3. Los Singletons que necesitan mantener referencias al Contexto deben utilizar applicationContext() o envolverlo en WeakReference
  4. Tener cuidado con las clases internas anónimas y noestáticas, ya que mantienen una referencia implícita a la clase que las rodea.
    1. Usa clases internas estáticas en lugar de anónimas si van a sobrevivir a la clase padre (como los Handlers).
    2. Si la clase interna o anónima es cancelable (como AsyncTask, Thread, RxSubscriptions), cancélala cuando se destruya la actividad.

Los componentes conscientes del ciclo de vida de Android

Una vez completados los pasos básicos anteriores, es el momento de algo mucho más importante: el ciclo de vida de las actividades de la app. Si no gestionamos el ciclo de vida correctamente, acabaremos aferrándonos a la memoria cuando ya no sea necesaria.

Hay muchas tareas diferentes involucradas en esto. Para cada actividad, necesitamos interrumpir hilos, deshacernos de las suscripciones en RxJava, cancelar las referencias AsyncTask y asegurarnos de que la referencia de esta actividad (y cualquier otra actividad conectada a ella) se elimina correctamente. Todas estas tareas pueden drenar una enorme cantidad de tiempo del desarrollador.

Las cosas se complican aún más por el Modelo Vista Presentador (MVP), el patrón arquitectónico a menudo empleado para construir la interfaz de usuario en Android. El MVP, sin embargo, es útil para desacoplar la lógica de negocio de la Vista.

En el patrón MVP, tanto la Vista como el Presentador son implementaciones abstractas de un contrato para un comportamiento entre ellos. La forma más común de implementar el MVP es utilizando una Actividad/Fragmento como implementación para la vista y una implementación simple para el presentador que está acostumbrado a tener una referencia de la Vista.

Así que terminamos teniendo una Vista con una referencia del Presentador y un Presentador con una Referencia de la Vista (Pista: Tenemos una fuga potencial aquí).

Dadas estas dificultades potenciales, es esencial que pongamos una estructura de gestión adecuada para eliminar el exceso de memoria creado durante el ciclo de vida. Hay varias maneras probadas de hacer esto:

Creación de componentes conscientes del ciclo de vida utilizando Android Arch Lifecycle en Android Studio

Los componentes conscientes del ciclo de vida son inteligentes. Pueden reaccionar a un cambio en el estado del ciclo de vida de otro componente, como actividades o fragmentos, deshaciéndose de la memoria, por ejemplo. Esto significa que el código es más ligero y mucho más eficiente en cuanto a memoria.

Arch Lifecycle es una nueva biblioteca de Android que ofrece un conjunto de herramientas para construir componentes conscientes del ciclo de vida. La biblioteca funciona de forma abstracta, lo que significa que los propietarios del ciclo de vida ya no tienen que preocuparse de gestionar el ciclo de vida de tareas y actividades específicas.

Las herramientas y definiciones clave de Arch Lifecycle son las siguientes:

  • LifeCycle: Un sistema de clasificación que define qué objetos tienen un ciclo de vida Android, y permite que luego sean monitoreados.
  • LifecycleObserver: Una interfaz regular que monitorea cada objeto identificado con un ciclo de vida de Android, utilizando una fórmula simple para manejar cada evento clave del ciclo de vida.
  • @OnLifecycleEvent: Una anotación que se puede utilizar en las clases que implementan la interfaz LifecycleObserver. Nos permite establecer los eventos clave del ciclo de vida que activarán el método anotado cada vez que se lance. Aquí hay una lista de todos los eventos que se pueden establecer:
    • ON_ANY
    • ON_CREATE
    • ON_DESTROY
    • ON_PAUSE
    • ON_RESUME
    • ON_START
    • ON_STOP
  • LifecycleOwner se implementa por defecto para cada componente de Android cuyo ciclo de vida puede ser gestionado, y da al desarrollador el control de cada evento.

Usando estas herramientas, somos capaces de enviar todas las tareas limpias a sus propietarios (presentadores en nuestro caso), por lo que tenemos un código limpio, desacoplado y libre de fugas (al menos en la capa del presentador).

Aquí tienes una implementación superbásica para que veas de lo que estamos hablando:

interface View: MVPView, LifecycleOwnerclass RandomPresenter : Presenter<View>, LifecycleObserver { private lateinit var view: View override fun attachView(view: View) { this.view = view view.lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.On_DESTROY) fun onClear() {//TODO: clean }

Usando Android Arch View Models como Presentadores y LiveData

Otra forma de evitar las fugas de memoria por mala gestión del ciclo de vida es usando los nuevos ViewModels de la Arch Components Library.

Un ViewModel es una clase abstracta que implementa una única función conocida como onClear, que se llama automáticamente cuando hay que eliminar un objeto concreto. El ViewModel es generado por el framework y está unido al ciclo de vida del creador (como ventaja añadida, es súper fácil de inyectar con Dagger.)

Además de utilizar ViewModel, LiveData también proporciona un canal vital de comunicación. El producto sigue un patrón reactivo y fácil de observar, lo que significa que los objetos individuales pueden observarlo (creando un código menos acoplado).

El gran punto aquí es que LiveData puede ser observado por un propietario del ciclo de vida, por lo que la transferencia de datos siempre es gestionada por el ciclo de vida -y podemos asegurarnos de que cualquier referencia es retenida mientras los usamos.

Usando LeakCanary y Bugfender

Además de los pasos mencionados, queríamos recomendar dos piezas importantes del kit: LeakCanary, una popular herramienta para monitorizar fugas, y nuestro propio Bugfender.

LeakCanary es una librería de detección de memoria para Android y Java. Es de código abierto, por lo que hay una gran comunidad detrás de ella, y no sólo le dice acerca de una fuga – que le informa de la causa probable.

Bugfender, nuestra herramienta de registro remoto, le permite depurar LeakTraces individuales y extender una clase llamada DisplayLeakService, que nos permite saber cuando se plantea una fuga. Entonces podemos registrarla con Bugfender fácilmente.

public class LeakUploadService extends DisplayLeakService { override fun afterDefaultHandling(heapDump: HeapDump, result: AnalysisResult, leakInfo: String) { if (result.leakFound) { Bugfender.d("LeakCanary", result.toString()) } }}

Además, los usuarios obtienen todos los demás beneficios de Bugfender, incluyendo el registro 24/7 (incluso cuando el dispositivo está desconectado), informes de fallos incorporados y una consola web fácil de usar, que permite profundizar en dispositivos individuales para una mejor atención al cliente.

Para más información sobre Bugfender, por favor haga clic aquí.

Leave a Reply