Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. ActionScript

Estabilice el uso de memoria del proyecto Flash con Object Pooling

by
Read Time:18 minsLanguages:

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

El uso de la memoria es un aspecto del desarrollo del que realmente debes tener cuidado, o podría terminar ralentizando tu aplicación, ocupando mucha memoria o incluso colapsando todo. ¡Este tutorial te ayudará a evitar esos malos resultados potenciales!


Vista previa del resultado final

Echemos un vistazo al resultado final en el que trabajaremos:

Haga clic en cualquier lugar del escenario para crear un efecto de fuego artificial y vigile el perfilador de memoria en la esquina superior izquierda.


Paso 1: Introducción

Si alguna vez ha perfilado su aplicación utilizando una herramienta de creación de perfiles o ha usado algún código o biblioteca que le indique el uso actual de memoria de su aplicación, es posible que haya notado que muchas veces el uso de memoria aumenta y luego vuelve a disminuir (si no lo ha hecho, su código es excelente!). Bueno, aunque estos picos causados ​​por el gran uso de la memoria se ven bastante bien, no son buenas noticias ni para su aplicación ni (en consecuencia) para sus usuarios. Sigue leyendo para entender por qué sucede esto y cómo evitarlo.


Paso 2: Uso bueno y malo

La imagen de abajo es un gran ejemplo de una mala gestión de la memoria. Es de un prototipo de un juego. Debe notar dos cosas importantes: los grandes picos en el uso de la memoria y el pico de uso de la memoria. ¡El pico está casi a 540Mb! Eso significa que este prototipo solo alcanzó el punto de usar 540Mb de la memoria RAM de la computadora del usuario, y eso es algo que definitivamente desea evitar.

Bad memory usageBad memory usageBad memory usage

Este problema comienza cuando empiezas a crear muchas instancias de objetos en tu aplicación. Las instancias no utilizadas seguirán usando la memoria de la aplicación hasta que el recolector de basura se ejecute, cuando se desasignen, lo que causará grandes picos. Una situación aún peor ocurre cuando las instancias simplemente no se desasignan, lo que hace que el uso de la memoria de la aplicación siga creciendo hasta que algo se bloquee o se rompa. Si desea saber más sobre el último problema y cómo evitarlo, lea este Consejo rápido sobre recolección de basura.

En este tutorial no abordaremos ningún problema del recolector de basura. En lugar de eso, trabajaremos en la construcción de estructuras que mantengan los objetos en la memoria de manera eficiente, haciendo que su uso sea completamente estable y evitando que el recolector de basura limpie la memoria, haciendo que la aplicación sea más rápida. Eche un vistazo al uso de memoria del mismo prototipo anterior, pero esta vez optimizado con las técnicas que se muestran aquí:

Good memory usageGood memory usageGood memory usage

Toda esta mejora se puede lograr utilizando la agrupación de objetos. Sigue leyendo para entender qué es y cómo funciona.


Paso 3: Tipos de Pools

La agrupación de objetos es una técnica en la que se crea un número predefinido de objetos cuando se inicializa la aplicación y se guarda en la memoria durante toda la vida útil de la aplicación. El grupo de objetos proporciona objetos cuando la aplicación los solicita, y restablece los objetos al estado inicial cuando la aplicación termina de usarlos. Hay muchos tipos de grupos de objetos, pero solo vamos a echar un vistazo a dos de ellos: los grupos de objetos estáticos y dinámicos.

El grupo de objetos estáticos crea un número definido de objetos y solo mantiene esa cantidad de objetos durante toda la vida útil de la aplicación. Si se solicita un objeto pero la agrupación ya ha otorgado todos sus objetos, la agrupación devuelve un valor nulo. Cuando se utiliza este tipo de grupo, es necesario abordar problemas como solicitar un objeto y no obtener nada a cambio.

La agrupación de objetos dinámicos también crea un número definido de objetos en la inicialización, pero cuando se solicita un objeto y la agrupación está vacía, la agrupación crea otra instancia automáticamente y devuelve ese objeto, aumentando el tamaño de la agrupación y agregándole el nuevo objeto.

En este tutorial construiremos una aplicación simple que genera partículas cuando el usuario hace clic en la pantalla. Estas partículas tendrán una vida útil limitada, y luego se eliminarán de la pantalla y se devolverán a la piscina. Para hacer eso, primero crearemos esta aplicación sin agrupar objetos y verificaremos el uso de la memoria, y luego implementaremos el conjunto de objetos y compararemos el uso de la memoria con anterioridad.


Paso 4: Aplicación inicial

Abra FlashDevelop (consulte esta guía) y cree un nuevo proyecto AS3. Usaremos un cuadrado pequeño de color simple como imagen de partícula, que se dibujará con código y se moverá según un ángulo aleatorio. Crea una nueva clase llamada Partícula que extienda Sprite. Asumiré que puede manejar la creación de una partícula y solo resaltar los aspectos que realizarán un seguimiento de la vida útil de la partícula y la eliminación de la pantalla. Puede obtener el código fuente completo de este tutorial en la parte superior de la página si tiene problemas para crear la partícula.

El código anterior es el código responsable de la eliminación de la partícula de la pantalla. Creamos una variable llamada _lifeTime para contener el número de milisegundos que la partícula estará en la pantalla. Inicializamos por defecto su valor a 1000 en el constructor. La función update() se llama a cada fotograma y recibe la cantidad de milisegundos que pasan entre los fotogramas, de modo que puede disminuir el valor de vida útil de la partícula. Cuando este valor llega a 0 o menos, la partícula solicita automáticamente a su padre que lo elimine de la pantalla. El resto del código se encarga del movimiento de la partícula.

Ahora haremos un montón de estos se crearán cuando se detecte un clic del ratón. Vaya a Main.as:

El código para actualizar las partículas debe ser familiar para usted: son las raíces de un simple bucle basado en el tiempo, comúnmente utilizado en los juegos. No olvide las declaraciones de importación:

Ahora puede probar su aplicación y crear un perfil utilizando el perfilador incorporado de FlashDevelop. Haga clic un montón de veces en la pantalla. Así es como se veía mi uso de memoria:

Unhandled memory usageUnhandled memory usageUnhandled memory usage

Hice clic hasta que el recolector de basura comenzó a correr. La aplicación creó más de 2000 partículas que fueron recolectadas. ¿Está empezando a parecerse al uso de memoria de ese prototipo? Lo parece, y esto definitivamente no es bueno. Para facilitar el perfilado, agregaremos la utilidad que se mencionó en el primer paso. Aquí está el código para agregar en Main.as:

¡No olvide importar net.hires.debug.Stats y está listo para ser utilizado!


Paso 5: Definiendo un Objeto Poolable

La aplicación que construimos en el Paso 4 era bastante simple. Presentaba solo un simple efecto de partículas, pero creaba muchos problemas en la memoria. En este paso, comenzaremos a trabajar en un grupo de objetos para solucionar ese problema.

Nuestro primer paso hacia una buena solución es pensar cómo se pueden agrupar los objetos sin problemas. En un grupo de objetos, debemos asegurarnos siempre de que el objeto creado esté listo para su uso y que el objeto devuelto esté completamente "aislado" del resto de la aplicación (es decir, no contiene referencias a otras cosas). Para forzar que cada objeto agrupado pueda hacer eso, vamos a crear una interface. Esta interfaz definirá dos funciones importantes que el objeto debe tener: renew() y destroy(). De esa manera, siempre podemos llamar a esos métodos sin preocuparnos de si el objeto los tiene o no (porque los tendrá). Esto también significa que cada objeto que queremos agrupar tendrá que implementar esta interfaz. Asi que aqui esta:

Ya que nuestras partículas serán poolables, debemos hacer que implementen IPoolable. Básicamente, movemos todo el código de sus constructores a la función renew(), y eliminamos cualquier referencia externa al objeto en la función destroy(). Esto es lo que debería ser:

El constructor tampoco debería requerir ningún argumento más. Si desea pasar cualquier información al objeto, deberá hacerlo a través de las funciones ahora. Debido a la forma en que funciona la función renew() ahora, también debemos establecer _destroyed en true en el constructor para que la función se pueda ejecutar.

Con eso, acabamos de adaptar nuestra clase Particle para que se comporte como un IPoolable. De esa manera, el conjunto de objetos podrá crear un conjunto de partículas.


Paso 6: Iniciar el conjunto de objetos

Ahora es el momento de crear un grupo de objetos flexible que pueda agrupar cualquier objeto que queramos. Este grupo actuará un poco como una fábrica: en lugar de utilizar la palabra clave new para crear objetos que puede utilizar, en su lugar, llamaremos a un método en el grupo que nos devuelve un objeto.

A efectos de simplicidad, el grupo de objetos será un Singleton. De esa manera podemos acceder a él desde cualquier lugar dentro de nuestro código. Comience creando una nueva clase llamada "ObjectPool" y agregando el código para convertirlo en un Singleton:

La variable _allowInstantiation es el núcleo de esta implementación de Singleton: es privada, por lo que solo la clase propia puede modificar, y el único lugar donde debe modificarse es antes de crear la primera instancia de la misma.

Ahora debemos decidir cómo mantener las piscinas dentro de esta clase. Dado que será global (es decir, puede agrupar cualquier objeto en su aplicación), primero tenemos que encontrar una manera de tener siempre un nombre único para cada grupo. ¿Como hacer eso? Hay muchas maneras, pero lo mejor que he encontrado hasta ahora es usar los propios nombres de clase de los objetos como el nombre del grupo. De esa manera, podríamos tener un grupo "Particle", un grupo de "Enemy" y así sucesivamente ... pero hay otro problema. Los nombres de clase solo tienen que ser únicos dentro de sus paquetes, por lo que, por ejemplo, se permitiría una clase "BaseObject" dentro del paquete "enemies" y una clase "BaseObject" dentro del paquete "structures". Eso causaría problemas en pool.

La idea de usar los nombres de clase como identificadores para las agrupaciones sigue siendo excelente, y aquí es donde flash.utils.getQualifiedClassName() nos ayuda. Básicamente, esta función genera una cadena con el nombre completo de la clase, incluidos los paquetes. ¡Ahora podemos usar el nombre de clase calificado de cada objeto como el identificador de sus grupos respectivos! Esto es lo que añadiremos en el siguiente paso.


Paso 7: Creando Pools

Ahora que tenemos una manera de identificar grupos, es hora de agregar el código que los crea. Nuestro grupo de objetos debe ser lo suficientemente flexible para admitir grupos tanto estáticos como dinámicos (hablamos de esto en el Paso 3, ¿recuerdas?). También debemos poder almacenar el tamaño de cada grupo y cuántos objetos activos hay en cada uno. Una buena solución para eso es crear una clase privada con toda esta información y almacenar todos los grupos dentro de un Object:

El código anterior crea la clase privada que contendrá toda la información sobre un grupo. También creamos el objeto _pools para contener todos los grupos de objetos. A continuación crearemos la función que registra un grupo en la clase:

Este código parece un poco más complicado, pero no te asustes. Todo está explicado aquí. La primera if parece realmente extraña. Es posible que nunca hayas visto esas funciones antes, así que esto es lo que hace:

  • La función describeType() crea un XML que contiene toda la información sobre el objeto que lo pasamos.
  • En el caso de una clase, todo está contenido dentro de la etiqueta factory.
  • Dentro de eso, el XML describe todas las interfaces que la clase implementa con la etiqueta implementsInterface.
  • Hacemos una búsqueda rápida para ver si la interfaz IPoolable se encuentra entre ellos. Si es así, entonces sabemos que podemos agregar esa clase a la agrupación, porque podremos convertirla con éxito como un IObject.

El código después de esta comprobación simplemente crea una entrada dentro de _pools si no existía una. Después de eso, el constructor PoolInfo llama a la función initialize() dentro de esa clase, creando efectivamente la agrupación con el tamaño que queremos. Ahora está listo para ser utilizado!


Paso 8: Obtener el objeto

En el último paso pudimos crear la función que registra un grupo de objetos, pero ahora necesitamos obtener un objeto para poder usarlo. Es muy sencillo: obtenemos un objeto si el grupo no está vacío y lo devolvemos. Si el pool está vacío, verificamos si es dinámico; si es así, aumentamos su tamaño, y luego creamos un nuevo objeto y lo devolvemos. Si no, devolvemos el nulo. (También puede optar por lanzar un error, pero es mejor simplemente devolver el valor nulo y hacer que su código resuelva esta situación cuando ocurra).

Aquí está la función getObj():

En la función, primero verificamos que la agrupación realmente existe. Suponiendo que se cumple esa condición, verificamos si la agrupación está vacía: si es pero es dinámica, creamos un nuevo objeto y la agregamos al pool. Si la agrupación no es dinámica, detendremos el código allí y simplemente devolveremos el valor nulo. Si el grupo aún tiene un objeto, tenemos el objeto más cercano al principio del grupo y llamamos renew(). Esto es importante: la razón por la que llamamos renew() en un objeto que ya estaba en el grupo es para garantizar que este objeto se dará en un estado "utilizable".

Probablemente se esté preguntando: ¿por qué no usa también esa comprobación genial con describeType() en esta función? Bueno, la respuesta es simple: describeType() crea un XML cada vez que lo llamamos, por lo que es muy importante evitar la creación de objetos que utilizan mucha memoria y que no podemos controlar. Además, solo basta con verificar si la agrupación realmente existe: si la clase aprobada no implementa IPoolable, eso significa que ni siquiera podríamos crear una agrupación. Si no hay un grupo para ello, entonces definitivamente detectamos este caso en nuestra declaración if al comienzo de la función.

¡Ahora podemos modificar nuestra clase Main y usar el grupo de objetos! Echale un vistazo:

Pulse compilar y perfilar el uso de la memoria! Esto es lo que tengo:

Very good memory usageVery good memory usageVery good memory usage

Eso es un poco genial, ¿no?


Paso 9: Devolviendo objetos al Pool

Hemos implementado con éxito un conjunto de objetos que nos da objetos. ¡Eso es increíble! Pero aún no ha terminado. Todavía estamos recibiendo solo objetos, pero nunca los devolvemos cuando ya no los necesitamos. Es hora de agregar una función para devolver objetos dentro de ObjectPool.as:

Revisemos la función: lo primero es verificar si hay un conjunto del objeto que se pasó. Está acostumbrado a ese código; la única diferencia es que ahora estamos usando un objeto en lugar de una clase para obtener el nombre calificado, pero eso no cambia la salida).

A continuación, obtenemos el índice del elemento en el grupo. Si no está en la piscina, simplemente lo ignoramos. Una vez que verifiquemos que el objeto está en la agrupación, debemos dividir la agrupación en la que se encuentra actualmente y volver a insertar el objeto al final de la misma. ¿Y por qué? Debido a que contamos los objetos usados ​​desde el principio del grupo, debemos reorganizar el grupo para que todos los objetos devueltos y no utilizados estén al final. Y eso es lo que hacemos en esta función.

Para grupos de objetos estáticos, creamos un objeto Vector que tiene una longitud fija. Debido a eso, no podemos splice() y push() los objetos hacia atrás. La solución a esto es cambiar la propiedad fixed de esos Vectors a false, eliminar el objeto y agregarlo de nuevo al final, y luego cambiar la propiedad de nuevo a true. También necesitamos disminuir el número de objetos activos. Después de eso, hemos terminado de devolver el objeto.

Ahora que hemos creado el código para devolver un objeto, podemos hacer que nuestras partículas regresen a la piscina una vez que alcancen el final de su vida útil. Dentro Particle.as:

Observe que agregamos una llamada a ObjectPool.instance.returnObj() allí. Eso es lo que hace que el objeto vuelva a la piscina. Ahora podemos probar y perfilar nuestra aplicación:

Awesome memory usageAwesome memory usageAwesome memory usage

¡Y ahí vamos! Memoria estable incluso cuando se hicieron cientos de clics!


Conclusión

Ahora sabe cómo crear y usar un conjunto de objetos para mantener estable el uso de memoria de su aplicación. La clase que construimos construida se puede usar en cualquier lugar y es realmente sencillo adaptar su código: al principio de su aplicación, cree grupos de objetos para cada tipo de objeto que quiera agrupar, y siempre que haya una palabra clave new(es decir, el creación de una instancia), reemplácela con una llamada a la función que obtiene un objeto para usted. ¡No olvide implementar los métodos que requiere la interfaz IPoolable!

Mantener su uso de memoria estable es realmente importante. Le ahorrará muchos problemas más adelante en su proyecto cuando todo comienza a desmoronarse con las instancias no recicladas que siguen respondiendo a los oyentes de eventos, los objetos que llenan la memoria que tiene disponible para usar y con el recolector de basura en funcionamiento y reduciendo la velocidad. Una buena recomendación es utilizar siempre la agrupación de objetos a partir de ahora, y notarás que tu vida será mucho más fácil.

Tenga en cuenta que, aunque este tutorial está dirigido a Flash, los conceptos desarrollados en él son globales: puede usarlo en aplicaciones de AIR, aplicaciones móviles y en cualquier lugar que se ajuste. ¡Gracias por leer!

Advertisement
Did you find this post useful?
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.