¿Cuándo es exactamente seguro usar las clases internas (anónimas)?

324

He estado leyendo algunos artículos sobre pérdidas de memoria 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 extensión anónima de la clase OnClickListenerviva más tiempo 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, ¿es peligroso este ejemplo 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 sobre el hecho de que comprender este tema tiene que ver con comprender en detalle lo que se guarda 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). ¿Cuándo super.onCreate(savedInstanceState)se llamará en mi onCreate(), esto restaurará los valores de los campos (como estaban antes del cambio de orientación)? ¿Esto también restaurará los estados de las clases internas?

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

Sébastien
fuente
14
Esta publicación de blog y esta publicación de blog tienen buena información sobre pérdidas de memoria y clases internas. :)
Alex Lockwood

Respuestas:

651

Lo que preguntas 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é lo mejor que pueda con el conocimiento de que tengo que cubrirlo y, con suerte, algunos otros se unirán para cubrir lo que pueda extrañar.

Clases Anidadas: Introducción

Como no estoy seguro de qué tan cómodo se siente con OOP en Java, esto afectará algunos aspectos básicos. Una clase anidada es cuando una definición de clase está contenida dentro de otra clase. Básicamente hay dos tipos: Clases anidadas estáticas y Clases internas. La verdadera diferencia entre estos son:

  • Clases Estáticas Anidadas:
    • Se consideran de "nivel superior".
    • No requiere que se construya una instancia de la clase que lo contiene.
    • No puede hacer referencia a los miembros de la clase que los contiene sin una referencia explícita.
    • Tener su propia vida.
  • Clases anidadas internas:
    • Siempre se requiere construir una instancia de la clase que lo contiene.
    • Tener automáticamente una referencia implícita a la instancia que lo contiene.
    • Puede acceder a los miembros de la clase del contenedor sin la referencia.
    • Se supone que la vida útil no debe ser más larga que la del contenedor.

Recolección de basura y clases internas

La recolección de basura es automática, pero intenta eliminar objetos según si cree que se están utilizando. El recolector de basura es bastante inteligente, pero no perfecto. Solo puede determinar si se está usando algo si existe o no una referencia activa al objeto.

El problema real aquí es cuando una clase interna se ha mantenido viva más tiempo que su contenedor. Esto se debe a la referencia implícita a la clase que lo contiene. La única forma en que esto puede ocurrir es si un objeto fuera de la clase que contiene mantiene 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 (por referencia) pero las referencias al objeto que lo contiene ya se han eliminado de todos los demás objetos. El objeto interno, por lo tanto, mantiene vivo al objeto contenedor porque siempre tendrá una referencia a él. El problema con esto es que, a menos que esté programado, no hay forma de volver al objeto que lo contiene para verificar si está vivo.

El aspecto más importante para esta realización es que no importa si se trata de una Actividad o de un sorteo. Usted siempre tiene que ser metódico cuando se utilizan las clases internas y asegurarse de que los objetos no sobreviven 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 filtraciones más difíciles de encontrar, ya que es probable que pasen desapercibidas hasta que muchas de ellas se hayan filtrado.

Soluciones: clases internas

  • Obtenga referencias temporales del objeto contenedor.
  • Permita que el objeto contenedor sea el único que mantenga 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 que lo contiene, considere convertirla en una clase estática.
  • Úselo 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 ejecutarse y visualizarse. Las actividades se definen por la característica de que deben tener una Vista. También tienen ciertos manejadores 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 hijos para poder mostrarla. Esto significa que cada Vista tiene una referencia a la Actividad (vía getContext()). Además, cada vista mantiene referencias a sus elementos secundarios (es decir getChildAt()). Finalmente, cada vista mantiene una referencia al mapa de bits representado que representa su visualización.

Siempre que tenga una referencia a una Actividad (o Contexto de Actividad), esto significa que puede seguir TODA la cadena en la jerarquía de diseño. Es por eso que las pérdidas de memoria con respecto a las actividades o vistas son tan importantes. Puede ser una tonelada de memoria que se filtró de una vez.

Actividades, vistas y clases internas

Dada la información anterior sobre las clases internas, estas son las pérdidas de memoria más comunes, pero también las que se evitan con mayor frecuencia. Si bien es deseable que una clase interna tenga acceso directo a los miembros de una clase de 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. Dado que muchas clases y métodos requieren un Contexto, los desarrolladores a veces intentarán guardar algo de código tomando una referencia a un Contexto y retenié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 necesita hacer. Si alguno de sus objetos tiene una referencia a una Actividad, su Contexto o cualquiera de sus Vistas cuando se destruye, acaba de filtrar esa Actividad y todo su árbol de Vistas.

Soluciones: actividades y puntos de vista

  • 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 duración (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 guardan referencias implícitamente.
  • Alternativamente, 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. Si bien puede hacer esto, es posible que desee consultar las prácticas anteriores.

Runnables: Introducción

Los runnables en realidad no son tan malos. Quiero decir, podrían ser, pero realmente ya hemos golpeado 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 las ejecutables se instancian desde el hilo de la interfaz de usuario. En esencia, usar un Runnable es crear otro hilo, solo un poco más administrado. Si clasifica un Runnable como una clase estándar y sigue las pautas anteriores, debería tener pocos problemas. La realidad es que muchos desarrolladores no hacen esto.

Debido a la facilidad, la legibilidad y el flujo lógico del programa, muchos desarrolladores utilizan clases internas anónimas para definir sus Runnables, como el ejemplo que creó anteriormente. Esto da como resultado un ejemplo como el que escribió anteriormente. Una clase interna anónima es básicamente una clase interna discreta. 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, lo que significa que mantiene una referencia implícita a su contenedor.

Runnables y Actividades / Vistas

¡Hurra! ¡Esta sección puede ser corta! Debido al hecho de que los Runnables se ejecutan fuera del hilo actual, el peligro con estos viene a ser operaciones asincrónicas de larga duración. Si el ejecutable se define en una Actividad o Vista como una Clase interna anónima O una Clase interna anidada, existen algunos peligros muy graves. Esto se debe a que, como se indicó anteriormente, tiene que saber quién es su contenedor. Ingrese el cambio de orientación (o la eliminación del sistema). Ahora solo consulte las secciones anteriores para comprender lo que acaba de suceder. Sí, tu ejemplo es bastante peligroso.

Soluciones: Runnables

  • Intente extender Runnable, si no rompe la lógica de su código.
  • Haga su mejor esfuerzo para hacer que los Runnables extendidos sean estáticos, si deben ser clases anidadas.
  • Si debe usar Runnables 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 AsyncTasks con la misma facilidad. Considere usar AsyncTask, ya que esos son VM Managed por defecto.

Respondiendo la pregunta final ahora para responder 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 tiempo que su clase externa?" Antes de llegar a esto, permítanme enfatizar de nuevo: aunque tiene razón en preocuparse por esto en Actividades, puede causar una fuga en cualquier lugar. Proporcionaré un ejemplo simple (sin usar una Actividad) solo para demostrarlo.

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 an Inner class
    public Leak createLeak()
    {
        return new Leak();
    }

// Mass Manufactured Leak class
    public class Leak
    {//Again for a little data.
       int size = 1;
    }
}

Este no es un ejemplo común, pero es 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 Factory, permanecerá en la memoria porque cada Leak tiene una referencia a ella. 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 al 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 una de esas nuevas LeakFactories acaba de filtrarse. ¿Qué piensas 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 lo peor que hubiera sido.

Conclusión

Estos enumeran los peligros principalmente conocidos de usar estos objetos de manera inapropiada. En general, esta publicación debería haber cubierto la mayoría de sus preguntas, pero entiendo que fue una publicación muuuy larga, así que si necesita aclaraciones, hágamelo saber. Siempre que siga las prácticas anteriores, tendrá muy poca preocupación por las fugas.

Fuzzical Logic
fuente
3
Muchas gracias por esta respuesta clara y detallada. Simplemente no entiendo lo que quieres decir con "muchos desarrolladores utilizan cierres para definir sus Runnables"
Sébastien
1
Los cierres en Java son clases internas anónimas, como el Runnable que usted describe. Es una forma de utilizar una clase (casi extenderla) sin escribir una Clase definida que extienda Runnable. Se llama cierre porque es "una definición de clase cerrada" en el sentido de que tiene su propio espacio de memoria cerrada dentro del objeto que lo contiene.
Fuzzical Logic
26
¡Escritura esclarecedora! Una observación con respecto a la terminología: no existe una clase interna estática en Java. ( Docs ). Una clase anidada es estática o interna , pero no puede ser ambas al mismo tiempo.
jenzz
2
Si bien eso es técnicamente correcto, Java le permite definir clases estáticas dentro de clases estáticas. La terminología no es para mi beneficio, sino para el beneficio de otros que no entienden la semántica técnica. Es por eso que se menciona por primera vez que son de "nivel superior". Los documentos para desarrolladores de Android también usan esta terminología, y esto es para las personas que buscan el desarrollo de Android, por lo que pensé que era mejor mantener la coherencia.
Fuzzical Logic
13
Gran publicación, una de las mejores en StackOverflow, especialmente para Android.
StackOverflowed