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


He estado leyendo algunos artículos sobre fugas 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 el usuario clases internas dentro de una Actividad.

Esto es lo que entendí:

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

En este ejemplo, supongo que no hay riesgo de fuga, porque no hay manera de que la clase anónima que se extiende OnClickListener viva 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, ¿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 con respecto al hecho de que entender este tema tiene que ver con entender en detalle lo que se mantiene cuando una actividad es destruida y recreada.

¿Lo es?

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

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

Author: gobernador, 2012-06-02

1 answers

Lo que estás haciendo 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é mi mejor esfuerzo con el conocimiento de que tengo que cubrirlo y, con suerte, algunos otros se unirán para cubrir lo que pueda perder.

Clases Anidadas: Introducción

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

  • Clases anidadas estáticas:
    • Se consideran de "nivel superior".
    • No requieren que se construya una instancia de la clase contenedora.
    • No puede hacer referencia a los miembros de la clase contenidos sin una referencia explícita.
    • Tienen su propia vida.
  • Clases Internas Anidadas:
    • Siempre requiere una instancia de la clase contenedora a construir.
    • Tiene automáticamente una referencia implícita a la instancia contenedora.
    • Puede acceder a los miembros de la clase del contenedor sin la referencia.
    • El tiempo de vida se supone que no es más largo que el del contenedor.

Recolección de Basura y Clases Internas

La recolección de basura es automática, pero intenta eliminar objetos en función de si piensa que están siendo utilizar. El Recolector de basura es bastante inteligente, pero no impecable. Solo puede determinar si algo está siendo utilizado por si hay o no una referencia activa al objeto.

El verdadero problema 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 contenedora. La única forma en que esto puede ocurrir es si un objeto fuera de la clase contenedora mantiene una referencia al objeto interno, sin tener en cuenta el objeto contenedor.

Esto puede llevar a una situación en la que el objeto interno está vivo (a través de 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 a él. El problema con esto es que a menos que esté programado, no hay forma de volver al objeto que lo contiene para comprobar si está vivo.

El aspecto más importante de esto la comprensión es que no hace ninguna diferencia si está en una actividad o es un elemento de diseño. Lo harás siempre debe ser metódico al usar clases internas y asegurarse de que nunca sobrevivan a los objetos del contenedor. Afortunadamente, si no es un objeto central de su código, las fugas 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: Interior Clases

  • Obtener referencias temporales del objeto que contiene.
  • Permita que el objeto que lo contiene sea el único que mantenga referencias de larga duración a los objetos internos.
  • Utilice patrones establecidos como la Fábrica.
  • Si la clase interna no requiere acceso a los miembros de la clase que la contienen, considere convertirla en una clase estática.
  • Úselo con precaución, independientemente de si está en una actividad o ni.

Actividades y Puntos de vista: Introducción

Las actividades contienen mucha información para poder ejecutarse y mostrarse. Las actividades se definen por la característica de que deben tener una Vista. También tienen ciertos manejadores automáticos. Tanto si se especifica como si 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 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 mantiene 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 toda la cadena hacia abajo en la jerarquía de diseño. Esta es la razón por la que las fugas de memoria con respecto a las Actividades o Vistas son un gran problema. Puede ser una tonelada de memoria se filtró todo a la vez.

Actividades, Vistas y Clases Internas

Dada la información anterior sobre las Clases Internas, estas son las fugas de memoria 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 una clase de Actividades, muchos están dispuestos a hacerlos estáticos para evitar problemas potenciales. El problema con las Actividades y Puntos de vista es mucho más profundo que eso.

Actividades Filtradas, Opiniones 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 agarrando una referencia a un Contexto y aferrándose a él. Sucede que muchos de los objetos que tenemos que crear para ejecutar nuestra Actividad tienen que existir fuera del ciclo de vida de la Actividad para permitir que la Actividad lo haga lo que tiene que 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 Vistas

  • Evite, a toda costa, hacer una referencia estática a una Vista o Actividad.
  • Todas las referencias a Contextos de Actividad deben ser de corta duración (la duración de la función)
  • Si necesita un contexto de larga duración, utilice 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 aún desee referirse a las prácticas anteriores.

Runnables: Introducción

Los runnables en realidad no son tan malos. Quiero decir, ellos podrían ser, pero en realidad ya hemos alcanzado la mayoría de las zonas de peligro. Un ejecutable es una operación asíncrona que realiza una tarea independiente del hilo en el que se creó. La mayoría de los runnables se instancian desde el subproceso de la interfaz de usuario. En esencia, usar un ejecutable es crear otro hilo, solo que un poco más administrado. Si clasifica un Ejecutable como una clase estándar y sigue las pautas anteriores, debería tener pocos problemas. La realidad es que muchos desarrolladores no hacen esto.

Fuera de la facilidad, legibilidad y flujo lógico del programa, muchos desarrolladores utilizan Clases Internas Anónimas para definir sus Runnables, como el ejemplo creado anteriormente. Esto resulta en un ejemplo como el que escribiste arriba. 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.

Ejecutables y Actividades/Vistas

Yay! Esta sección puede ser corta! Debido al hecho de que los Runnables se ejecutan fuera del subproceso actual, el peligro con estos viene a las operaciones asíncronas 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, hay algunos peligros muy graves. Esto se debe a que, como se dijo anteriormente, ha para saber quién es su contenedor. Introduzca el cambio de orientación (o eliminación del sistema). Ahora solo refiérase a las secciones anteriores para entender 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 todo lo posible para que los Runnables extendidos sean estáticos, si deben ser clases anidadas.
  • Si debe usar Runnables anónimos, evite crearlos en cualquiera objeto que tiene un referencia de larga duración a una Actividad o Vista que está en uso.
  • Muchos Runnables podrían haber sido tan fácilmente AsyncTasks. Considere el uso de AsyncTask, ya que son Máquinas virtuales administradas de forma predeterminada.

Respuesta a la Pregunta Final Ahora para responder a las preguntas que no fueron directamente abordadas por las otras secciones de este post. 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 recalcar: aunque usted tienen razón en preocuparse por esto en las 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 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 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 ninguna fábrica. A pesar de que lanzamos la Fábrica, permanecerá en la memoria porque cada fuga tiene una referencia a ella. Ni siquiera importa que la clase externa no tenga datos. Esto sucede con mucha más frecuencia de lo que uno podría pensar. No necesitamos al creador, solo sus creaciones. Así que creamos uno temporalmente, pero usamos las creaciones indefinidamente.

Imagine 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 ser filtrada. ¿Qué piensas de eso? Esos son dos ejemplos muy comunes de cómo una clase interna puede sobrevive a una clase externa de cualquier tipo. Si esa clase externa hubiera sido una Actividad, imagina lo mucho peor que habría sido.

Conclusión

Estos enumeran los peligros principalmente conocidos de usar estos objetos de manera inapropiada. En general, este post debería haber cubierto la mayoría de sus preguntas, pero entiendo que fue un post loooong, así que si necesita una aclaración, solo hágamelo saber. Mientras siga las prácticas anteriores, tendrá muy poca preocupación por las fugas.

 569
Author: Fuzzical Logic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-30 11:11:51