Consideraciones de rendimiento sobre fugas de memoria: un libro de cocina de Android Parte 2 | por mvndy | octubre 2022

Contenidos

Extraña interacción fragmento/vista del ciclo de vida, fugas de rx y fugas de dependencia

Este artículo es una continuación de la Parte 1 de esta miniserie de Memory Leaks Cookbook para Android. Hablar de fugas de memoria a veces implica más que el problema técnico en sí.Por un lado, la definición de una fuga de memoria es subjetiva. Los autores de Programación de Android con Kotlin: Cómo lograr la concurrencia estructurada tienden a ser más cautelosos con respecto a las fugas de memoria, especialmente para bases de código más grandes:

  • Si el montón contiene la memoria asignada más tiempo del necesario
  • Cuando un objeto está asignado en la memoria, pero es inalcanzable para el programa actual

Otro problema es que los análisis de ruptura de OOM a veces indican un síntoma del problema real: que la aplicación ya ha ocupado la mayor parte de la memoria asignada en el cuadro de mando del dispositivo a continuación:6. Primitivos de subprocesos almacenados estáticamente dentro de singletons → eliminar7. Ver oyentes + miembros en fragmento → cancelar en onDestroyView8. Fugas de prescripción -> Devolver resultados en hilo principal + eliminar desechables

6. Primitivos de subprocesos almacenados estáticamente dentro de singletons → quitar

Este ejemplo se presenta en el contexto de Dagger 2/Hilt, pero los conceptos detrás de esta pérdida de memoria se pueden aplicar a cualquier forma de inyección de dependencia. La anotación @Singleton es en realidad un alcance. El alcance determina cuánto tiempo se mantiene viva una adicción. En el caso de un objeto anotado con @Singleton, se mantiene vivo durante el tiempo de vida del componente que puede estar usándolo. ¿Qué hace que esto sea una pérdida de memoria? La anotación @Singleton podría considerarse un «objeto de dios». Entonces, ¿qué importa que ThreadPoolExecutor esté siempre presente durante la vida útil del montón? La respuesta radica en cuántas tareas se llevan a cabo dentro de ThreadPoolExecutor. Supongamos que ponemos la dependencia TopologicalProcessor en ambos actividad principal y algunos casos de Segunda actividad para que podamos alimentar tareas de carga de mosaicos de mapas en tileThreadPoolExecutor en la inicialización. En tiempo de ejecución, un usuario se sienta en el 1) actividad principal pantalla, 2) abre una instancia de Segunda actividad3) lo cierra con la navegación hacia atrás, luego 4) abre otra instancia de Segunda actividad una vez más.Una representación visual de actividades y Runnable continuamente activo en cola dada la pérdida de memoria actual. Al examinar el registro truncado para ver qué tareas Runnable se almacenan en la cola de tileMapThreadPoolExecutor y se ejecutan, podemos ver 3 tareas que se crean poco después de que se inicia MainActivity y se ejecutan inmediatamente después. Luego, SecondActivity, que agrega su propio conjunto de tareas ejecutables a la cola. Pero al acceder a la cola de tileMapThreadPoolExecutor, encontramos que la cola aún contiene las mismas tareas ejecutables que MainActivity ya agregó. El resultado es que todas las tareas se vuelven a ejecutar, aunque ya las habíamos ejecutado previamente y cada tarea debería haberse descartado después de completar el trabajo.Ya estamos viendo problemas, pero sigamos leyendo hasta el último bloque de registro. SecondActivity se destruye, luego se inicia otra instancia de SecondActivity. La nueva SecondActivity agrega su propio conjunto de tareas ejecutables a la cola. Sin embargo, la cola no descartó las otras tareas que ya se estaban ejecutando y luego se puso en cola en Agregado al intentar vaciar la cola. Como podemos ver, este problema puede encarecerse muy rápidamente. Evite almacenar primitivos de subprocesamiento de datos de Android con la palabra clave astatic en Java o en un objeto complementario en Kotlin. ¡No queremos subprocesos que vivan para siempre y que GC no pueda eliminar! Eliminar la palabra clave estática o mover el miembro de la clase fuera del objeto complementario brinda una solución simple al problema, como se muestra en el fragmento de código a continuación: Ahora hemos movido tileMapThreadPoolExecutor fuera del objeto complementario. Al realizar el mismo conjunto de interacciones (abrir una instancia de SecondActivity, cerrar y abrir una nueva instancia de SecondActivity), ahora podemos ver en el registro que las tareas completadas también se eliminaron en la memoria.Ahora vemos que cada vez que se abre una clase de actividad, no se ejecutan tareas duplicadas. Dado que cada ejecutable se pone en cola al finalizar el trabajo, vaciar el tileMapThreadPoolExecutor no genera subprocesos innecesarios para el trabajo ya completado.Una representación visual de la navegación del usuario en la memoria y el orden adecuado de las tareas ejecutables en la cola ThreadPoolExecutor. Para los pocos afortunados que encuentran esto en sus bases de código, esta solución devuelve tanta memoria que será difícil no declarar esto como una victoria, ¡así que asegúrese de medir el montón antes y después!

7. Oyentes + Vistas en Fragment → Anular referencias en Fragment::onDestroyView

Si no se eliminan las referencias de vista en Fragment::onDestroyView, esas vistas se mantendrán usando la pila de tareas. Esto puede no ser un gran problema para aplicaciones más pequeñas, pero para aplicaciones grandes, estas pequeñas fugas pueden acumularse y causar OOM. Anteriormente, esto no estaba claro en la documentación, pero los desarrolladores de Android deben tener en cuenta este comportamiento previsto: la vista de un fragmento (pero no el fragmento en sí) se destruye cuando un fragmento se inserta en la pila posterior. Debido a esto, se espera que los desarrolladores eliminen o anulen las referencias de las vistas en Fragment::onDestroyView. Gracias a PY por rastrear esta fuga de memoria. Como puede ver, las fugas de memoria son un tema muy debatido: en este caso, programar Android con Kotlin en realidad lo consideraría una fuga de memoria, ya que las vistas no se limpian hasta que el fragmento en sí se destruye de forma permanente. Afortunadamente, hay una solución simple para esto: invalidar todos los miembros de Vista dentro de un Fragmento en onDestroyView. Del mismo modo, los enlaces de vista y los oyentes declarados como miembros de clase también deben invalidarse en Fragment::onDestroyView. Hacer un cambio de código con el menor tiempo y riesgo posible es una gran victoria por poco costo y esfuerzo que vale la pena presumir.

8. Fugas de Rx -> devolver los resultados al hilo principal + eliminar los elementos desechables del ciclo de vida

Trabajar con RxJava puede ser difícil. Por el bien de la conversación, sigamos con el contexto de RxJava 2. Hay dos reglas simples cuando se trabaja con CompositeDisposable, las cuales se pueden cubrir con el siguiente ejemplo de código, que muestra que CompositeDisposable se encuentra en una capa de moderador. Este ejemplo solo muestra trabajar con un desechable, pero nuestras pérdidas de memoria ya existen por breves que sean. ¿Puedes identificar las dos fuentes de fuga?1. Devolver los resultados del flujo de eventos al hilo principal al final de la cadena Rx – de lo contrario, su resultado convertido podría desaparecer en la parte inferior de los subprocesos de fondo y causar pérdidas de memoria (o peor aún, se bloquea). utilizable para viewstate: 2. Deseche los artículos desechables. Cancela tus suscripciones. Si queremos suscribirnos a CompositeDisposable en el contexto de un componente de Android, asegúrese de eliminar la suscripción al final de la vida útil para evitar fugas. En el caso de nuestro fragmento de código actual, hacemos la llamada clara a nuestro CompositeDisposable cuando la vista adjunta al presentador ha terminado su vida. ¿Has visto alguno de estos cambios simples en tu base de código? Si es así, puede reparar su propia fuga de memoria y buscar diferencias en el uso de la memoria tomando una grabación .hprof con Memory Profiler en Android Studio. También puede importar su grabación .hprof para profundizar con el analizador de memoria de Eclipse, o explorar otras herramientas de rendimiento de código abierto como Perfetto, etc. ¿Quiere comprender la mecánica de ThreadPoolExecutor y otras primitivas de subprocesamiento de datos? ¿Entiende las peculiaridades de los ciclos de vida en colisión en los componentes de Android? Si disfrutó de este artículo, consulte Programación de Android con Kotlin: Cómo lograr una concurrencia estructurada con Coroutines, que se publicó recientemente, para obtener consideraciones más detalladas sobre el rendimiento de Android y la administración de la memoria en torno a la concurrencia. Esta serie de artículos también está vinculada a la presentación Droidcon NYC 2022 Fugas de memoria y consideraciones de rendimiento: un libro de cocina.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.