() translation by (you can also view the original English article)
¿Alguna vez has utilizado una aplicación Flash y has notado lag en ella? ¿Todavía no sabes por qué ese genial juego flash se ejecuta lentamente en tu ordenador? Si quieres saber más sobre una posible causa de ello, entonces este artículo es para ti.
Encontramos a este impresionante autor gracias a FlashGameLicense.com, ¡el lugar para comprar y vender juegos Flash!
Cada pocas semanas, revisamos algunas de las publicaciones favoritas de nuestros lectores a lo largo de la historia del sitio. Este tutorial se publicó por primera vez en junio de 2010.
Avance del resultado final
Veamos el resultado final para el que vamos a trabajar:
Paso 1: Un rápido repaso a las referencias
Antes de entrar en materia, hay que saber un poco cómo funciona la instanciación y la referenciación en AS3. Si ya has leído sobre el tema, aún así te recomiendo que leas este pequeño paso. ¡Así, todo el conocimiento estará fresco en tu cabeza y no tendrás problemas para leer el resto de este Consejo Rápido!
La creación y referencia de instancias en AS3 es diferente de lo que la mayoría de la gente piensa. La instanciación (o "creación") de algo ocurre solo cuando el código pide crear un objeto. Normalmente, esto ocurre a través de la palabra clave "new", pero también está presente cuando se utiliza una sintaxis literal o se definen parámetros para las funciones, por ejemplo. A continuación se muestran ejemplos de ello:
1 |
// Instantiation through the "new" keyword
|
2 |
new Object(); |
3 |
new Array(); |
4 |
new int(); |
5 |
new String(); |
6 |
new Boolean(); |
7 |
new Date(); |
8 |
|
9 |
// Instantiation through literal syntax
|
10 |
{}; |
11 |
[]; |
12 |
5
|
13 |
"Hello world!"
|
14 |
true
|
15 |
|
16 |
// Instantiation through function parameters
|
17 |
private function tutExample(parameter1:int, parameter2:Boolean):void |
Después de crear un objeto, éste permanecerá solo hasta que algo lo referencie. Para ello, generalmente se crea una variable y se le pasa el valor del objeto a la variable, para que sepa qué objeto contiene actualmente. Sin embargo (y esta es la parte que la mayoría de la gente desconoce), cuando pasas el valor de una variable a otra variable, no estás creando un nuevo objeto. En cambio, estás creando otro enlace con el objeto que ahora tienen ambas variables. Mira la imagen de abajo para aclararlo:



La imagen supone que tanto la Variable 1 como la Variable 2 pueden contener el smiley (es decir, pueden contener el mismo tipo). En el lado izquierdo, solo existe la Variable 1. Sin embargo, cuando creamos y establecemos la Variable 2 con el mismo valor de la Variable 1, no estamos creando un vínculo entre la Variable 1 y la Variable 2 (parte superior derecha de la imagen), sino que estamos creando un vínculo entre el Smiley y la Variable 2 (parte inferior derecha de la imagen).
Con este conocimiento, podemos saltar al Colector de Basura.
Paso 2: Toda ciudad necesita un recolector de basura
Es obvio que toda aplicación necesita una cierta cantidad de memoria para funcionar, ya que necesita variables para mantener los valores y utilizarlos. Lo que no está claro es cómo la aplicación gestiona los objetos que ya no son necesarios. ¿Los recicla? ¿Los borra? ¿Deja el objeto en la memoria hasta que se cierra la aplicación? Las tres opciones pueden ocurrir, pero aquí hablaremos específicamente de la segunda y la tercera.
Imagina una situación en la que una aplicación crea muchos objetos cuando se inicializa, pero una vez que este periodo termina más de la mitad de los objetos creados quedan sin usar. ¿Qué pasaría si se dejaran en la memoria? Seguramente ocuparían mucho espacio en ella, provocando lo que la gente llama lag, que es una notable ralentización de la aplicación. A la mayoría de los usuarios no les gustaría esto, así que debemos evitarlo. ¿Cómo podemos codificar para que la aplicación se ejecute de forma más eficiente? La respuesta está en el recolector de basura.
El recolector de basura es una forma de gestión de la memoria. Su objetivo es eliminar cualquier objeto que no se utilice y que esté ocupando espacio en la memoria del sistema. De esta manera, la aplicación puede funcionar con un uso mínimo de memoria. Veamos cómo funciona:



Cuando tu aplicación comienza a ejecutarse, pide al sistema una cantidad de memoria que será utilizada por la aplicación. La aplicación comienza entonces a llenar esta memoria con cualquier información que necesite; cada objeto que creas va a parar a ella. Sin embargo, si el uso de la memoria se acerca a la memoria solicitada inicialmente, el recolector de basura se ejecuta, buscando cualquier objeto no utilizado para vaciar algún espacio en la memoria. A veces esto causa un poco de retraso en la aplicación, debido a la gran sobrecarga de la búsqueda de objetos.
En la imagen, puedes ver los picos de memoria (marcados con un círculo verde). Los picos y la caída repentina son causados por el recolector de basura, que actúa cuando la aplicación ha alcanzado el uso de memoria solicitado (la línea roja), eliminando todos los objetos innecesarios.
Paso 3: Iniciar el archivo SWF
Ahora que sabemos lo que el Garbage Collector puede hacer por nosotros, es el momento de aprender a codificar para obtener todos sus beneficios. En primer lugar, tenemos que saber cómo funciona el Garbage Collector, desde un punto de vista práctico. En el código, los objetos se convierten en elegibles para la recolección de basura cuando se vuelven inalcanzables. Cuando no se puede acceder a un objeto, el código entiende que ya no se va a utilizar, por lo que hay que recogerlo.
Actionscript 3 comprueba la accesibilidad a través de las raíces de recogida de basura. En el momento en que no se puede acceder a un objeto a través de una raíz de recogida de basura, se convierte en elegible para la recogida. A continuación se muestra una lista de las principales raíces de recogida de basura:
- Variables a nivel de paquete y estáticas.
- Variables locales y variables en el ámbito de un método o función en ejecución.
- Variables de instancia de la instancia de la clase principal de la aplicación o de la lista de visualización.
Para entender cómo los objetos son manejados por el Garbage Collector, debemos codificar y examinar lo que sucede en el archivo de ejemplo. Utilizaré el proyecto AS3 de FlashDevelop y el compilador de Flex, pero asumo que puedes hacerlo en cualquier IDE que quieras, ya que no vamos a utilizar cosas específicas que solo existen en FlashDevelop. He construido un archivo sencillo con una estructura de botones y texto. Como este no es el objetivo en este consejo rápido, lo explicaré rápidamente: cuando se hace clic en un botón, se dispara una función. En cualquier momento que queramos mostrar algún texto en la pantalla, se llama a una función con el texto y se muestra. También hay otro campo de texto para mostrar una descripción de los botones.
El objetivo de nuestro archivo de ejemplo es crear objetos, eliminarlos y examinar qué ocurre con ellos después de ser eliminados. Necesitaremos una forma de saber si el objeto está vivo o no, así que añadiremos un listener ENTER_FRAME a cada uno de los objetos, y haremos que muestren algún texto con el tiempo que han estado vivos. ¡Así que vamos a codificar el primer objeto!
He creado una divertida imagen sonriente para los objetos, en homenaje al gran tutorial del juego Avoider de Michael James Williams, que también utiliza imágenes sonrientes. Cada objeto tendrá un número en su cabeza, para que podamos identificarlo. Además, he llamado al primer objeto TheObject1, y al segundo objeto TheObject2, para que sea fácil de distinguir. Vamos al código:
1 |
private var _theObject1:TheObject1; |
2 |
|
3 |
private function newObjectSimple1(e:MouseEvent):void |
4 |
{
|
5 |
// If there is already an object created, do nothing
|
6 |
if (_theObject1) |
7 |
return; |
8 |
|
9 |
// Create the new object, set it to the position it should be in and add to the display list so we can see it was created
|
10 |
_theObject1 = new TheObject1(); |
11 |
_theObject1.x = 320; |
12 |
_theObject1.y = 280; |
13 |
_theObject1.addEventListener(Event.ENTER_FRAME, changeTextField1); |
14 |
|
15 |
addChild(_theObject1); |
16 |
}
|
El segundo objeto es casi igual. Aquí está:
1 |
private var _theObject2:TheObject2; |
2 |
|
3 |
private function newObjectSimple2(e:MouseEvent):void |
4 |
{
|
5 |
// If there is already an object created, do nothing
|
6 |
if (_theObject2) |
7 |
return; |
8 |
|
9 |
// Create the new object, set it to the position it should be in and add to the display list so we can see it was created
|
10 |
_theObject2 = new TheObject2(); |
11 |
_theObject2.x = 400; |
12 |
_theObject2.y = 280; |
13 |
_theObject2.addEventListener(Event.ENTER_FRAME, changeTextField2); |
14 |
|
15 |
addChild(_theObject2); |
16 |
}
|
En el código, newObjectSimple1() y newObjectSimple2() son funciones que se disparan cuando se pulsa su correspondiente botón. Estas funciones simplemente crean un objeto y lo añaden en la pantalla de visualización, por lo que sabemos que fue creado. Además, crea un escuchador de eventos ENTER_FRAME en cada objeto, lo que hará que muestren un mensaje cada segundo, mientras estén activos. Estas son las funciones:
1 |
private function changeTextField1(e:Event):void |
2 |
{
|
3 |
// Our example is running at 30FPS, so let's add 1/30 on every frame in the count.
|
4 |
_objectCount1 += 0.034; |
5 |
|
6 |
// Checks to see if _objectCount1 has passed one more second
|
7 |
if(int(_objectCount1) > _secondCount1) |
8 |
{
|
9 |
// Displays a text in the screen
|
10 |
displayText("Object 1 is alive... " + int(_objectCount1)); |
11 |
|
12 |
_secondCount1 = int(_objectCount1); |
13 |
}
|
14 |
}
|
1 |
private function changeTextField2(e:Event):void |
2 |
{
|
3 |
// Our example is running at 30FPS, so let's add 1/30 on every frame in the count.
|
4 |
_objectCount2 += 0.034; |
5 |
|
6 |
// Checks to see if _objectCount2 has passed one more second
|
7 |
if(int(_objectCount2) > _secondCount2) |
8 |
{
|
9 |
// Displays a text in the screen
|
10 |
displayText("Object 2 is alive... " + int(_objectCount2)); |
11 |
|
12 |
_secondCount2 = int(_objectCount2); |
13 |
}
|
14 |
}
|
Estas funciones simplemente muestran un mensaje en la pantalla con el tiempo que los objetos han estado vivos. Aquí está el archivo SWF con el ejemplo actual:
Paso 4: Borrar los objetos
Ahora que hemos cubierto la creación de objetos, vamos a intentar algo: ¿te has preguntado alguna vez qué pasaría si realmente eliminas (quitas todas las referencias) un objeto? ¿Se recoge la basura? Eso es lo que vamos a probar ahora. Vamos a construir dos botones de borrado, uno para cada objeto. Vamos a hacer el código para ellos:
1 |
private function deleteObject1(e:MouseEvent):void |
2 |
{
|
3 |
// Check if _theObject1 really exists before removing it from the display list
|
4 |
if (_theObject1 && contains(_theObject1)) |
5 |
removeChild(_theObject1); |
6 |
|
7 |
// Removing all the references to the object (this is the only reference)
|
8 |
_theObject1 = null; |
9 |
|
10 |
// Displays a text in the screen
|
11 |
displayText("Deleted object 1 successfully!"); |
12 |
}
|
1 |
private function deleteObject2(e:MouseEvent):void |
2 |
{
|
3 |
// Check if _theObject2 really exists before removing it from the display list
|
4 |
if (_theObject1 && contains(_theObject2)) |
5 |
removeChild(_theObject2); |
6 |
|
7 |
// Removing all the references to the object (this is the only reference)
|
8 |
_theObject2 = null; |
9 |
|
10 |
// Displays a text in the screen
|
11 |
displayText("Deleted object 2 successfully!"); |
12 |
}
|
Echemos un vistazo a la SWF ahora. ¿Qué crees que pasará?
Como puedes ver. Si haces clic en "Crear Objeto1" y luego en "Borrar Objeto1", ¡no pasa nada! Podemos decir que el código se ejecuta, porque el texto aparece en la pantalla, pero ¿por qué no se borra el objeto? El objeto sigue ahí porque no se ha eliminado realmente. Cuando borramos todas las referencias a él, le dijimos al código que lo hiciera elegible para la recolección de basura, pero el recolector de basura nunca se ejecuta. Recuerda que el recolector de basura solo se ejecutará cuando el uso de memoria actual se acerque a la memoria solicitada cuando la aplicación comenzó a ejecutarse. Tiene sentido, pero ¿cómo vamos a probar esto?
Desde luego, no voy a escribir un trozo de código que llene nuestra aplicación de objetos inútiles hasta que el uso de la memoria sea demasiado grande. En su lugar, utilizaremos una función actualmente no admitida por Adobe, según el artículo de Grant Skinner, que obliga a ejecutar el recolector de basura. De esta forma, podemos activar este sencillo método y ver qué ocurre cuando se ejecuta. Además, a partir de ahora, me referiré al Garbage Collector como GC, por razones de simplicidad. Esta es la función:
1 |
private function forceGC(e:MouseEvent):void |
2 |
{
|
3 |
try
|
4 |
{
|
5 |
new LocalConnection().connect('foo'); |
6 |
new LocalConnection().connect('foo'); |
7 |
}
|
8 |
catch (e:*) { } |
9 |
|
10 |
// Displays a text in the screen
|
11 |
displayText("----- Garbage collection triggered -----"); |
12 |
}
|
Esta simple función, que solo crea dos objetos LocalConnection(), es conocida por forzar la ejecución de la GC, por lo que la llamaremos cuando queramos que esto ocurra. No recomiendo usar esta función en una aplicación seria. Si lo haces para probar, no hay problemas reales, pero si es para una aplicación que se distribuirá a la gente, esta no es una buena función para usar, ya que puede incurrir en efectos negativos.
Lo que recomiendo para casos como este es que dejes que la GC funcione a su propio ritmo. No intentes forzarla. En su lugar, céntrate en codificar de forma eficiente para que no se produzcan problemas de memoria (lo veremos en el paso 6). Ahora, echemos un vistazo a nuestro SWF de ejemplo de nuevo, y hagamos clic en el botón "Recoger basura" después de crear y eliminar un objeto.
¿Has probado el archivo? ¡Ha funcionado! Puedes ver que ahora, después de borrar un objeto y activar la GC, ¡elimina el objeto! Fíjate que si no eliminas el objeto y llamas a la GC, no pasará nada, ya que sigue habiendo una referencia a ese objeto en el código. Ahora, ¿qué pasa si intentamos mantener dos referencias a un objeto y eliminamos una de ellas?
Paso 5: Crear otra referencia
Ahora que hemos comprobado que la GC funciona exactamente como queríamos, vamos a probar otra cosa: vincular otra referencia a un objeto (Objeto1) y eliminar la original. En primer lugar, debemos crear una función para vincular y desvincular una referencia a nuestro objeto. Hagámoslo:
1 |
private function saveObject1(e:MouseEvent):void |
2 |
{
|
3 |
// _onSave is a Boolean to check if we should link or unlink the reference
|
4 |
if (_onSave) |
5 |
{
|
6 |
// If there is no object to save, do nothing
|
7 |
if (!_theObject1) |
8 |
{
|
9 |
// Displays a text in the screen
|
10 |
displayText("There is no object 1 to save!"); |
11 |
|
12 |
return; |
13 |
}
|
14 |
|
15 |
// A new variable to hold another reference to Object1
|
16 |
_theSavedObject = _theObject1; |
17 |
|
18 |
// Displays a text in the screen
|
19 |
displayText("Saved object 1 successfully!"); |
20 |
|
21 |
// On the next time this function runs, unlink it, since we just linked
|
22 |
_onSave = false; |
23 |
}
|
24 |
else
|
25 |
{
|
26 |
// Removing the reference to it
|
27 |
_theSavedObject = null; |
28 |
|
29 |
// Displays a text in the screen
|
30 |
displayText("Unsaved object 1 successfully!"); |
31 |
|
32 |
// On the next time this function runs, link it, since we just unlinked
|
33 |
_onSave = true; |
34 |
}
|
35 |
}
|
Si ahora probamos nuestro swf, nos daremos cuenta de que si creamos el Objeto1, luego lo guardamos, lo borramos y forzamos la ejecución de la GC, no pasará nada. Esto se debe a que ahora, aunque hayamos eliminado el enlace "original" al objeto, sigue existiendo otra referencia al mismo, lo que hace que no sea elegible para la recolección de basura. Esto es básicamente todo lo que necesitas saber sobre el recolector de basura. No es un misterio, después de todo. Pero, ¿cómo aplicar esto a nuestro entorno actual? ¿Cómo podemos usar este conocimiento para evitar que nuestra aplicación se ejecute lentamente? Esto es lo que nos mostrará el Paso 6: cómo aplicar esto en ejemplos reales.
Paso 6: Hacer que tu código sea eficiente
Ahora viene la mejor parte: ¡hacer que tu código funcione con la CG de forma eficiente! Este paso te proporcionará información útil que deberías conservar durante toda tu vida: ¡guárdala bien! En primer lugar, me gustaría presentar una nueva forma de construir tus objetos en tu aplicación. Es una forma simple, pero efectiva de colaborar con el GC. Esta forma introduce dos clases simples, que pueden ser ampliadas a otras, una vez que entiendas lo que hace.
La idea de este modo es implementar una función -llamada destroy(), en cada objeto que crees, y llamarla cada vez que termines de trabajar con un objeto. La función contiene todo el código necesario para eliminar todas las referencias hacia y desde el objeto (excluyendo la referencia que se utilizó para llamar a la función), por lo que te aseguras de que el objeto sale de tu aplicación totalmente aislado, y es fácilmente reconocido por el GC. La razón de esto se explica en el siguiente paso. Veamos el código general de la función:
1 |
// Create this in every object you use
|
2 |
public function destroy():void |
3 |
{
|
4 |
// Remove event listeners
|
5 |
// Remove anything in the display list
|
6 |
// Clear the references to other objects, so it gets totally isolated
|
7 |
|
8 |
}
|
9 |
|
10 |
// ...
|
11 |
// When you want to remove the object, do this:
|
12 |
theObject.destroy(); |
13 |
|
14 |
// And then null the last reference to it
|
15 |
theObject = null; |
En esta función, tendrás que borrar todo del objeto, para que quede aislado en la aplicación. Después de hacer esto, será más fácil para el GC localizar y eliminar el objeto. Ahora veamos algunas de las situaciones en las que se producen la mayoría de los errores de memoria:
- Objetos que se utilizan solo en un intervalo de ejecución: ten cuidado con estos, ya que pueden ser los que consuman mucha memoria. Estos objetos existen solo durante algún periodo de tiempo (por ejemplo, para almacenar valores cuando se ejecuta una función) y no se accede a ellos con mucha frecuencia. Recuerda eliminar todas las referencias a ellos después de que hayas terminado con ellos, de lo contrario puedes tener muchos de ellos en tu aplicación, solo ocupando espacio en la memoria. Ten en cuenta que si creas muchas referencias a ellas, debes eliminar cada una a través de la función destroy().
- Objetos que quedan en la lista de visualización: siempre quita un objeto de la lista de visualización si quieres eliminarlo. La lista de visualización es una de las raíces de la recolección de basura (¿lo recuerdas?) y por eso es realmente importante que mantengas tus objetos lejos de ella cuando los elimines.
- Referencias al escenario, al padre y a la raíz: si te gusta usar mucho estas propiedades, recuerda eliminarlas cuando termines. Si muchos de tus objetos tienen una referencia a éstas, ¡puedes tener problemas!
- Escuchadores de eventos: a veces la referencia que evita que tus objetos sean recogidos es un escuchador de eventos. Recuerda eliminarlos, o utilizarlos como oyentes débiles, si es necesario.
- Arrays y vectores: a veces tus arrays y vectores pueden tener otros objetos, dejando referencias dentro de ellos de las que puedes no ser consciente. ¡Ten cuidado con los arrays y los vectores!
Paso 7: La isla de las referencias
Aunque trabajar con la CG es genial, no es perfecto. Tienes que prestar atención a lo que estás haciendo, de lo contrario pueden ocurrir cosas malas con tu aplicación. Me gustaría demostrar un problema que puede surgir si no sigues todos los pasos necesarios para que tu código funcione correctamente con la CG.
A veces, si no borras todas las referencias hacia y desde un objeto, puedes tener este problema, especialmente si enlazas muchos objetos en tu aplicación. A veces, puede bastar con que quede una sola referencia para que esto ocurra: todos tus objetos forman una isla de referencias, en la que todos los objetos están conectados a otros, no permitiendo que la GC los elimine.
Cuando el GC se ejecuta, realiza dos tareas sencillas para comprobar si hay objetos que eliminar. Una de estas tareas es contar cuántas referencias tiene cada objeto. Todos los objetos con 0 referencias se recogen al mismo tiempo. La otra tarea es comprobar si hay algún pequeño grupo de objetos que se enlazan entre sí, pero a los que no se puede acceder, desperdiciando así la memoria. Comprueba la imagen:



Como puedes ver, los objetos verdes no pueden ser alcanzados, pero su recuento de referencias es 1. El GC realiza la segunda tarea para comprobar este trozo de objetos y los elimina todos. Sin embargo, cuando el chunk es demasiado grande, el GC "abandona" la comprobación y asume que los objetos pueden ser alcanzados. Ahora imagina que tienes algo así:



Esta es la isla de las referencias. Tomaría mucha memoria del sistema, y no sería recogida por el GC debido a su complejidad. Suena bastante mal, ¿no? Sin embargo, se puede evitar fácilmente. Solo tienes que asegurarte de que has borrado todas las referencias hacia y desde un objeto, ¡y entonces no ocurrirán cosas espantosas como esta!
Conclusión
Esto es todo por ahora. En este Quick Tip aprendimos que podemos hacer nuestro código mejor y más eficiente para reducir el lag y los problemas de memoria, haciéndolo así más estable. Para ello, tenemos que entender cómo funcionan los objetos de referencia en AS3, y cómo aprovecharlos para que la GC funcione correctamente en nuestra aplicación. A pesar de que podemos mejorar nuestra aplicación, tenemos que tener cuidado al hacerlo, ¡de lo contrario puede volverse aún más desordenada y lenta!
Espero que te haya gustado este sencillo consejo. Si tienes alguna pregunta, ¡deja un comentario abajo!