¿Cuándo es exactamente seguro usar una fuga (anónima) de las clases internas?

He estado leyendo algunos artículos sobre memory leaks en Android y vi este interesante video de Google I / O sobre el tema .

Aún así, no entiendo completamente el concepto, y especialmente cuando es seguro o peligroso para las clases internas del usuario dentro de una Actividad .

Esto es lo que entendí:

Se producirá una pérdida de memoria si una instancia de una clase interna sobrevive más tiempo que su clase externa (una Actividad). -> ¿ En qué situaciones puede suceder esto?

En este ejemplo, supongo que no hay riesgo de fuga, porque no hay forma de que la clase anónima que se extiende OnClickListener más que la actividad, ¿verdad?

  final Dialog dialog = new Dialog(this); dialog.setContentView(R.layout.dialog_generic); Button okButton = (Button) dialog.findViewById(R.id.dialog_button_ok); TextView titleTv = (TextView) dialog.findViewById(R.id.dialog_generic_title); // *** Handle button click okButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { dialog.dismiss(); } }); titleTv.setText("dialog title"); dialog.show(); 

Ahora, ¿este ejemplo es peligroso y por qué?

 // We are still inside an Activity _handlerToDelayDroidMove = new Handler(); _handlerToDelayDroidMove.postDelayed(_droidPlayRunnable, 10000); private Runnable _droidPlayRunnable = new Runnable() { public void run() { _someFieldOfTheActivity.performLongCalculation(); } }; 

Tengo una duda con respecto al hecho de que la comprensión de este tema tiene que ver con la comprensión detallada de lo que se conserva cuando una actividad se destruye y se vuelve a crear.

¿Lo es?

Digamos que acabo de cambiar la orientación del dispositivo (que es la causa más común de fugas). Cuando se super.onCreate(savedInstanceState) en mi onCreate() , ¿restaurará esto los valores de los campos (como estaban antes de que cambie la orientación)? ¿Esto también restaurará los estados de las clases internas?

Me doy cuenta de que mi pregunta no es muy precisa, pero realmente agradecería cualquier explicación que pueda aclarar las cosas.

Sebastian,

Lo que estás preguntando es una pregunta bastante difícil. Si bien puede pensar que es solo una pregunta, en realidad está haciendo varias preguntas a la vez. Haré todo lo posible con el conocimiento de que tengo que cubrirlo y, con suerte, otros se unirán para cubrir lo que pueda extrañar.

Clases internas: Introducción

Como no estoy seguro de lo cómodo que está con OOP en Java, esto afectará un par de conceptos básicos. Una clase interna es cuando una definición de clase está contenida dentro de otra clase. Básicamente hay dos tipos: estático y no estático. La verdadera diferencia entre estos son:

  • Clases internas estáticas:
    • Se consideran de “alto nivel”.
    • No requiere que se construya una instancia de la clase contenedora.
    • No puede hacer referencia a los miembros de la clase que lo contienen sin una referencia explícita.
    • Tener su propia vida.
  • Clases internas no estáticas:
    • Siempre requiere una instancia de la clase contenedora que se construirá.
    • Automáticamente tiene una referencia implícita a la instancia que contiene.
    • Puede acceder a los miembros de la clase del contenedor sin la referencia.
    • Se supone que Lifetime no debe ser más largo que el del contenedor.

Recolección de basura y clases internas no estáticas

La recolección de basura es automática pero trata de eliminar objetos en función de si cree que se están utilizando. El recolector de basura es bastante inteligente, pero no perfecto. Solo puede determinar si algo está siendo usado ya sea que haya o no una referencia activa al objeto.

El verdadero problema aquí es cuando una clase interna no estática se ha mantenido con vida más tiempo que su contenedor. Esto se debe a la referencia implícita a la clase contenedora. La única forma en que esto puede ocurrir es si un objeto fuera de la clase contenedora guarda una referencia al objeto interno, sin tener en cuenta el objeto que lo contiene.

Esto puede conducir a una situación en la que el objeto interno está vivo (a través de la referencia) pero las referencias al objeto que lo contiene ya se han eliminado de todos los demás objetos. El objeto interno es, por lo tanto, mantener vivo el objeto que lo contiene porque siempre tendrá una referencia al mismo. El problema con esto es que, a menos que esté progtwigdo, no hay forma de volver al objeto que lo contiene para verificar si aún está vivo.

El aspecto más importante para esta realización es que no importa si está en una actividad o si es dibujable. Siempre tendrá que ser metódico cuando use clases internas no estáticas y asegúrese de que nunca vivan más que los objetos del contenedor. Afortunadamente, si no es un objeto central de su código, las filtraciones pueden ser pequeñas en comparación. Desafortunadamente, estas son algunas de las fugas más difíciles de encontrar, porque es probable que pasen desapercibidas hasta que muchas de ellas se hayan filtrado.

Soluciones: Clases internas no estáticas

  • Obtenga referencias temporales del objeto que contiene.
  • Permita que el objeto contenedor sea el único que guarde referencias duraderas a los objetos internos.
  • Use patrones establecidos como la fábrica.
  • Si la clase interna no requiere acceso a los miembros de la clase contenedora, considere convertirla en una clase estática.
  • Usar con precaución, independientemente de si está en una actividad o no.

Actividades y Vistas: Introducción

Las actividades contienen mucha información para poder ejecutar y mostrar. Las actividades se definen por la característica de que deben tener una Vista. También tienen ciertos controladores automáticos. Ya sea que lo especifique o no, la Actividad tiene una referencia implícita a la Vista que contiene.

Para que se cree una Vista, debe saber dónde crearla y si tiene algún elemento secundario para que pueda mostrarse. Esto significa que cada Vista tiene una referencia a la Actividad (a través de getContext() ). Además, cada Vista mantiene referencias a sus hijos (es decir, getChildAt() ). Finalmente, cada Vista guarda una referencia al Mapa de bits renderizado que representa su visualización.

Siempre que tenga una referencia a una Actividad (o Contexto de actividad), esto significa que puede seguir la cadena ENTERA en la jerarquía de diseño. Esta es la razón por la cual las pérdidas de memoria con respecto a las Actividades o Vistas son un gran problema. Puede ser una gran cantidad de memoria que se filtra todo de una vez.

Actividades, vistas y clases internas no estáticas

Dada la información anterior acerca de Inner Classes, estas son las memory leaks más comunes, pero también las más comúnmente evitadas. Si bien es deseable que una clase interna tenga acceso directo a los miembros de la clase Actividades, muchos están dispuestos a hacerlos estáticos para evitar posibles problemas. El problema con Actividades y Vistas es mucho más profundo que eso.

Actividades filtradas, vistas y contextos de actividad

Todo se reduce al Contexto y al Ciclo de Vida. Hay ciertos eventos (como la orientación) que matarán un contexto de actividad. Como tantas clases y métodos requieren un contexto, los desarrolladores intentarán a veces guardar un código tomando una referencia a un contexto y manteniéndolo. Sucede que muchos de los objetos que tenemos que crear para ejecutar nuestra actividad tienen que existir fuera del Activity LifeCycle para permitir que la actividad haga lo que debe hacer. Si alguno de tus objetos tiene una referencia a una Actividad, su Contexto o cualquiera de sus Vistas cuando se destruye, acabas de filtrar esa Actividad y todo su Árbol de vistas.

Soluciones: actividades y vistas

  • Evite, a toda costa, hacer una referencia estática a una vista o actividad.
  • Todas las referencias a los contextos de actividad deben ser de corta vida (la duración de la función)
  • Si necesita un contexto de larga duración, use el contexto de la aplicación ( getBaseContext() o getApplicationContext() ). Estos no mantienen referencias implícitamente.
  • De forma alternativa, puede limitar la destrucción de una actividad anulando los cambios de configuración. Sin embargo, esto no impide que otros eventos potenciales destruyan la Actividad. Mientras puede hacer esto, puede que quiera consultar las prácticas anteriores.

Ejecutables: Introducción

Runnables en realidad no son tan malos. Quiero decir, podrían serlo, pero realmente ya hemos llegado a la mayoría de las zonas de peligro. Un Runnable es una operación asincrónica que realiza una tarea independiente del hilo en el que se creó. La mayoría de los ejecutables se instancian desde el hilo de UI. En esencia, usar Runnable está creando otro hilo, solo un poco más administrado. Si clasifica un Runnable como una clase estándar y sigue las pautas anteriores, debe encontrar pocos problemas. La realidad es que muchos desarrolladores no hacen esto.

Sin facilidad, legibilidad y flujo de progtwig lógico, muchos desarrolladores utilizan clases internas anónimas para definir sus ejecutables, como el ejemplo que creaste arriba. Esto da como resultado un ejemplo como el que escribiste arriba. Una clase interna anónima es básicamente una clase interna discreta no estática. Simplemente no tiene que crear una definición completamente nueva y simplemente anular los métodos apropiados. En todos los demás aspectos, es una clase interna no estática, lo que significa que mantiene una referencia implícita a su contenedor.

Runnables y Actividades / Vistas

¡Hurra! ¡Esta sección puede ser corta! Debido a que Runnables se ejecuta fuera del subproceso actual, el peligro con estos se refiere a las operaciones asincrónicas de larga ejecución. Si el ejecutable se define en una actividad o vista como una clase interna anónima o una clase interna no estática, existen algunos peligros muy graves. Esto se debe a que, como se dijo anteriormente, tiene que saber quién es su contenedor. Ingrese el cambio de orientación (o muerte del sistema). Ahora solo refiérase a las secciones anteriores para entender lo que acaba de pasar. Sí, tu ejemplo es bastante peligroso.

Soluciones: Runnables

  • Pruebe y extienda Runnable, si no rompe la lógica de su código.
  • Haga todo lo posible para hacer static Runnables extendidos, si deben ser clases internas.
  • Si debe utilizar Ejecutables anónimos, evite crearlos en cualquier objeto que tenga una referencia de larga duración a una Actividad o Vista que esté en uso.
  • Muchos Runnables podrían haber sido fácilmente AsyncTasks. Considere usar AsyncTask ya que esos son Gestionados por VM de manera predeterminada.

Respondiendo a la última pregunta ahora para responder a las preguntas que no fueron abordadas directamente por las otras secciones de esta publicación. Usted preguntó “¿Cuándo puede un objeto de una clase interna sobrevivir más que su clase exterior?” Antes de llegar a esto, permítanme volver a enfatizar: aunque tiene razón al preocuparse por esto en Actividades, puede causar una fuga en cualquier lugar. Proporcionaré un ejemplo simple (sin usar una Actividad) solo para demostrar.

A continuación se muestra un ejemplo común de una fábrica básica (falta el código).

 public class LeakFactory {//Just so that we have some data to leak int myID = 0; // Necessary because our Leak class is non-static public Leak createLeak() { return new Leak(); } // Mass Manufactured Leak class public class Leak {//Again for a little data. int size = 1; } } 

Este es un ejemplo no tan común, pero lo suficientemente simple como para demostrarlo. La clave aquí es el constructor …

 public class SwissCheese {//Can't have swiss cheese without some holes public Leak[] myHoles; public SwissCheese() {//Gotta have a Factory to make my holes LeakFactory _holeDriller = new LeakFactory() // Now, let's get the holes and store them. myHoles = new Leak[1000]; for (int i = 0; i++; i<1000) {//Store them in the class member myHoles[i] = _holeDriller.createLeak(); } // Yay! We're done! // Buh-bye LeakFactory. I don't need you anymore... } } 

Ahora, tenemos fugas, pero no fábrica. Aunque lanzamos la fábrica, permanecerá en la memoria porque cada fuga tiene una referencia. Ni siquiera importa que la clase externa no tenga datos. Esto sucede mucho más a menudo de lo que uno podría pensar. No necesitamos el creador, solo sus creaciones. Entonces creamos uno temporalmente, pero usamos las creaciones indefinidamente.

Imagina lo que sucede cuando cambiamos el constructor solo ligeramente.

 public class SwissCheese {//Can't have swiss cheese without some holes public Leak[] myHoles; public SwissCheese() {//Now, let's get the holes and store them. myHoles = new Leak[1000]; for (int i = 0; i++; i<1000) {//WOW! I don't even have to create a Factory... // This is SOOOO much prettier.... myHoles[i] = new LeakFactory().createLeak(); } } } 

Ahora, cada uno de esos nuevos LeakFactories acaba de filtrarse. ¿Qué piensa usted de eso? Esos son dos ejemplos muy comunes de cómo una clase interna puede sobrevivir a una clase externa de cualquier tipo. Si esa clase externa hubiera sido una Actividad, imagina cuánto hubiera sido peor.

Conclusión

Estos enumeran los peligros conocidos principalmente de usar estos objetos inapropiadamente. En general, esta publicación debería haber cubierto la mayoría de tus preguntas, pero entiendo que fue una publicación exagerada, así que si necesitas una aclaración, házmelo saber. Mientras siga las prácticas anteriores, tendrá muy poca preocupación de fugas.

Espero que esto ayude,

FuzzicalLogic