Gestión de Memoria y ARC

Tutorial Swift – Gestión de Memoria y ARC

Josué V. Herrera Swift Avanzado, Tutoriales 0 Comments

En este Tutorial Swift aprenderemos sobre la gestión de memoria y de manera inevitable también sobre el Contador Automático de Referencia o como mejor se le conoce ARC (Automatic Reference Counting).

Comencemos por recordar que as aplicaciones que se ejecutan en los ordenadores, los móviles, tabletas, televisores, etc… hacen uso de la memoria incorporada en estos y la gran mayoría la gestiona de manera dinámica: asigna (allocate) y libera (deallocate) los datos almacenados en segmentos de la memoria según necesite.

La postura de Swift sobre la gestión de la memoria es relativamente única ya que como en otros lenguajes, la mayoría de los problemas relacionados con la memoria son manejados automáticamente pero al mismo tiempo y a diferencia de esos otros lenguajes Swift no usa un garbage collector (Recolector de Basura), en lugar de esto usa un sistema de conteo de referencias el cual estudiaremos en este artículo de hoy, veremos como funciona y todo cuanto tenemos que tener en cuenta para evitar fugas de memoria.

Asignación y Liberación de Memoria

La asignación y el manejo de memoria en los tipos por valor y por referencia en el lenguaje Swift no se lleva a acabo de la misma manera. En el caso de los tipos por valor es bien sencilla: se reserva un segmento de memoria para cada instancia, cuando la almacenamos en una propiedad o incluso cuando la pasamos a una función se crea una copia de la misma, memoria que es liberada una vez terminada la ejecución de la función o el ámbito donde se encuentra la propiedad  ha finalizado, así de manera automática sin que nosotros tengamos que hacer algo al respecto.

En este tutorial nos enfocaremos en las clases cuya asignación funciona similar a la de los tipos por valor pero que a diferencia de este último cuando pasamos nuestra instancia a una función o la almacenamos en una propiedad no se crea una copia de la instancia, en lugar de esto, se genera una referencia al espacio de memoria que fue asignado para la instancia de la clase. Es decir que la instancia de una clase en sí, representa un apuntador a su espacio de memoria y cuando suceden los eventos antes comentados se crea una referencia adicional que apunta al mismo espacio de memoria. En este punto podemos encontrarnos ante  la clásica duda:

¿Qué sucedería si una de las referencias modifica una propiedad miembro de la clase?

Pues este cambio sería común para el resto de referencias que apuntan al mismo segmento de memoria.

Como podemos constatar este comportamiento es bastante similar a lo que vemos en otros lenguajes pero que a diferencia de estos Swift no requiere que nosotros gestionemos la memoria de manera manual, al mismo tiempo que tampoco contamos con un garbage collector cuidando de estos detalles en tiempo de ejecución.  Los creadores de Swift idearon un sistema que se implementa en tiempo de compilación y donde a cada instancia de clase se le asigna un contador de referencias, el cual representa el número de referencias existentes que apuntan al espacio de memoria de la instancia. Dado que las referencias generan dependencia sobre nuestra instancia, los recursos de la misma no serán liberados hasta que el contador de referencia no sea igual a cero, momento en el que el método deinit es ejecutado.

Todo esto comenzó hace un tiempo atrás no muy lejano, donde las aplicaciones en Objective-C también usaban el conteo de referencia pero a diferencia del actual este era manual, es decir que este sistema requería (como su nombre indica) de una gestión manual sobre el conteo de referencias. Cada clase tenía un método llamado retain que se encargaba de reclamar y conservar la propiedad sobre el objeto al mismo tiempo que incrementaba el contador de referencias en uno. En conjunto con el método retain también estaba otro de nombre release cuya función era la inversa: liberaba o renunciaba a su propiedad sobre la instancia disminuyendo el contador de referencias en uno. Como podremos imaginarnos el conteo de referencias manual trae consigo muchos errores, ya sea si hacemos retain demasiadas veces sobre una instancia y luego no podemos desasignar esa memoria se generaba un memory leak (Fuga de Memoria) y así también nos encontrábamos errores cuando ejecutando release sobre un segmento de memoria que ya había sido liberado y quizás hasta reasignado, etc.

Debido a lo anteriormente comentado y como parte también de la evolución natural de la tecnología, Apple en el año 2011 lanza ARC para Objective-C. Luego de su implementación el compilador pasaba a ser el encargado de analizar el código y de insertar (como ya hemos comentado) las llamadas a retain y a release en todos los lugares apropiados. Igual sucede con Swift que ha sido construido de la mano con ARC, no tenemos que gestionar la asignación o liberación de la memoria de manera manual. Aun así es muy bueno entender como este sistema trabaja y de hecho en pos de evitar problemas con la gestión de memoria, sí pues aunque ARC es bastante inteligente hay situaciones en las que ciertos enfoques de diseño pueden generar problemas relativamente importantes.

Ciclos de Referencia Fuerte

Hablemos del Strong Reference Cycles pero antes crearemos una aplicación de consola donde probaremos los ejemplos siguientes:

Aquí tenemos una clase de nombre Person en representación de una persona, cuenta con una propiedad llamada name donde almacenaremos el nombre de la persona. Esta clase implementa el protocolo CustomStringConvertible y como tal se define la propiedad computada description. El bloque init inicializa la propiedad name y el último bloque de nombre deinit se ejecuta cuando la instancia es destruida, pero si aplicamos lo que hemos comentado anteriormente pudiéramos decir que el bloque deinit se ejecuta cuando el contador de referencias de la instancia ha llegado a cero. Probemos esto añadiendo el siguiente código al final del anterior:

…la salida en pantalla sería:

Primero hemos creado una instancia de Person, luego la igualamos a nil y vemos en la salida como acto seguido el bloque deinit se ha ejecutado, luego al intentar imprimir la descripción de la instancia obtenemos que esta es nil, es decir que ya no existe.

La variable Gochi es una instancia opcional de la clase Person y por defecto todas las referencias que creamos son referencias fuertes o como me gusta más llamarle strong references. Esto significa que el contador de referencias se incrementa en uno automáticamente al crear la instancia Gochi.

Veamos ahora otra clase:

La clase Asset es muy similar a la anterior, la diferencia principal sería la línea 5 donde tenemos una propiedad opcional de tipo Person. Veamos en este punto que sucede si hacemos uso de esta clase en conjunto con el ejemplo anterior y sin modificar mucho:

Luego de hacer esto tendríamos la salida en pantalla:

La memoria ocupada por el objeto Asset (Cowboy Hat, valor 150.0, no tiene propietario) está siendo liberada. La memoria ocupada por el objeto Asset (Black Backpack, valor 30.0, no tiene propietario) está siendo liberada.

Hasta aquí nada nuevo, pero aún no hemos usado la instancia opcional de Person que forma parte de la clase Asset, pero si analizamos un poco nos daremos cuenta que nuestros assets pueden tener dueño pero aún las personas no son conscientes de sus posesiones. Así que necesitamos implementar un mecanismo en Person que nos permita vincularnos con los assets que hemos comprado y al mismo tiempo que estos guarden un registro de sus propietarios. Modifiquemos la clase Person para que luzca como la siguiente:

En la línea 4 hemos añadido un arreglo de tipo Asset que almacenará las propiedades de la persona en cuestión. También hemos añadido de la línea 24 a la 29 una nueva función que se encarga de hacernos tomar posesión sobre cierto asset. Tal y como podemos ver en las siguientes líneas:

Ahora, cuando unimos todos estos cambios:

…la salida en pantalla es la siguiente:

Sorpresa! cierto?

Solamente se ha destruido la instancia de la clase Asset correspondiente a la mochila negra (black backpack) la cual fue el único asset que no se le adjudicó a la instancia de Person y al mismo tiempo nos percatamos que la instancia gochi tampoco ha sido liberada luego de haberse igualado a nil.

¿Que sucedió?

Para entenderlo comencemos por analizar la siguiente imagen:

Referencias Strong - Asset y Person

…en esta podemos ver el estado donde nos encontramos en la línea 49, es decir, este sería el estado antes de aplicar nil a las instancias de Asset tal y como sucede en las líneas 50, 51 y 52. De izquierda a derecha comenzamos por la variable opcional gochi de tipo Gochi la cual como instancia constituye una referencia a su propio espacio de memoria. Luego de las líneas 47 a la 49 creamos tres instancias de Asset las cuales están representadas a la derecha del diagrama. En las líneas 51 y 52 establecemos a gochi como propietario de los assets Macbook Pro y Cowboy Hat mediante el método takeOwnershipOfAsset, en este igualamos / copiamos la propia instancia (self (en este caso de gochi)) hacia la propiedad de nombre owner, miembro de Asset y evidentemente de tipo Person:

En este punto hemos creado una referencia desde la instancia de Asset hacia la de Person, luego en la siguiente:

…añadimos el asset en cuestión al arreglo de assets (una persona puede poseer más de un objeto) declarado en la línea 6 de la clase Person y con esta acción creamos una referencia desde la instancia gochi hacia las instancias de laptop y hat.

Me gustaría aclarar que en el gráfico anterior las flechas entrantes son las referencias que generan dependencia y efectivamente las que incrementan el contador de referencias. Cuando digo dependencia me refiero a que una instancia determinada apunta hacia nosotros, necesita de nosotros para su funcionamiento por lo que si dejamos de existir esta se quedaría apuntando a un espacio de memoria que ya no contiene información válida y con esto vendría consigo los respectivos errores.

Esta es la razón por la que existe el contador de referencias, en la imagen podemos observar que el espacio de memoria correspondiente a la instancia de Person está siendo usada por dos instancias de Asset algo que impide que la ejecución de la instrucción de la línea 46 tenga efecto. Así que pudiéramos pensar que eliminando (igualándolas a nil) las instancias de Asset sería suficiente, ¿verdad? Pues no, ya que al mismo tiempo desde Person (el arreglo assets) tenemos otras dos referencias hacia las instancias de Asset y esto impide que la memoria de estas pueda ser liberada, no hay una salida aparente, es un problema cíclico del cual no podemos salir y de ahí su nombre: Ciclos de Referencia Fuerte (Strong Reference Cycles).

Los Ciclos de Referenca Fuerte hay que evitarlos a toda costa ya que aparte que representan un error de diseño (no necesariamente lógico) también constituyen un tipo de fuga de memoria. En nuestro ejemplo hemos asignado la memoria necesaria para las instancias de Person y Asset pero nuestro programa jamás retornó esa memoria al sistema operativo.

Referencias weak

Para todo casi siempre hay una solución aún cuando es desconocida, en este caso la solución es romper el ciclo y para esto contamos con la palabra clave weak que significa débil en inglés. Creo que dicho esto ya deben de estar suponiendo a que se debe su nombre.

Una referencia débil (weak) al contrario de la fuerte (strong) no incrementa el contador de referencias de la instancia a la cual apunta.

Hagamos lo siguiente: declaremos como weak a la propiedad llamada owner en la clase Asset (línea 5):

Cuando hacemos esto y ejecutamos la función / método takeOwnershipOfAsset, específicamente la línea 26, el comportamiento que logramos es que el contador de referencias de la instancia gochi no se incremente en uno, por lo que la única referencia que tendría Person sería su propia instancia que al ser igualada a nil dejaría de existir. Es decir que luego de esta modificación si ejecutamos nuevamente el programa la salida en pantalla sería:

En esta podemos corroborar que los ciclos de referencia fuerte se han roto. Algo importante a comentar sería que en el caso de no haber igualado a nil las instancias de Asset, la propiedad owner luego de desaparecer la referencia destino (Gochi) sería igual a nil. Por este motivo es que en Swift las propiedades marcadas como weak tienen que ser variables y opcionales, no pueden ser constantes dado su posible cambio a nil y opcionales pues para que puedan representar el estado mismo de nil.

El siguiente diagrama resume lo antes expuesto, muestra como se encuentran las referencias luego de aplicar weak a la propiedad owner de la clase Asset:

Referencias Weak - Asset y Person

Las líneas discontinuas representan las referencias weak, al mismo tiempo en la clase Person observamos que solamente cuenta con una referencia, la de su propia instancia. Tal y como explicamos las referencias weak no incrementan el contador de referencias, son referencias débiles que pueden romperse en cualquier momento y por ende no generan dependencia.

Diseño de clases teniendo en cuenta la gestión de memoria

Objetos como un sombrero, una mochila y una laptop pueden existir sin dueño, aún olvidadas o botadas seguirán ahí en algún rincón del basurero. Teniendo esto en cuenta tiene mucho sentido que la referencia hacia la clase Person sea weak, mientras que la de Person  hacia Asset sea strong. La persona es la que decide hasta cuando les serán útiles estas pertenencias y llegado el momento será el único (como propietario) que podrá romper esos lazos de propiedad. Es decir que si ejecutamos algo como:

…sin antes hacer un:

La memoria de la instancia laptop no se liberaría ya que la instancia gochi tiene una referencia fuerte hacia esta, es decir que laptop depende de gochi ya que lógicamente ella sola (no tiene conciencia) no puede determinar que ya no tiene dueño.

Dependencias obligatorias y unidireccionales

Una persona es dueño de una tarjeta de crédito pero a su vez la tarjeta de crédito no puede existir sin un dueño. Analizando rápidamente esto generaría una configuración donde la tarjeta de crédito tendría una referencia fuerte hacia Person ya que esta sin un propietario no existiría. Al mismo tiempo una persona puede no tener una tarjeta de crédito, por lo que este campo sería variable y opcional, una referencia weak. Veámoslo en el siguiente diagrama:

Relacion Incorrecta - Person y Creditcard

Este enfoque es incorrecto ya que parte del análisis donde una persona puede o no tener una tarjeta de crédito y esto es cierto, el problema reside en la lógica de que una tarjeta no puede poseer a una persona (aunque algunos bancos así lo quisieran), una persona puede existir sin tarjeta de crédito por lo que la dependencia generada con esta configuración, por más que pueda ser posible, no es para nada óptima. Otro detalle a tener en cuenta sería que Person tiene dos referencias mientras que CreditCard solamente una, así que para que Person deje de tener una tarjeta de crédito esta tendría que (mágicamente (no tiene conciencia)) romper su lazo de propiedad y no al contrario como debería de ser.

Referencias unowned

En este punto llegan las referencias unowned las cuales nos permiten lograr el mismo comportamiento que las weak pero a diferencia de estas pueden ser constantes y por ende no opcionales. Estas referencias vienen a ayudarnos en ocasiones bien determinadas, siempre con la premisa de que su valor jamás mutará a nil y esto es un beneficio que va a depender bastante de nuestro diseño, de lo que estemos intentando lograr.

Por ejemplo si usamos una referencia unowned en la solución que estamos implementando entre las clases Person y CreditCard, nos ayudaría a que la dependencia recayera sobre la tarjeta de crédito en lugar de la persona. En el caso de la tarjeta de crédito tendríamos la seguridad de que su propiedad miembro owner de tipo Persona jamás será nil, ya que una tarjeta no puede existir sin dueño, mientras que en el caso de gochi su referencia a la tarjeta será fuerte pero opcional. El código quedaría de la siguiente forma:

En le caso de Person:

Luego de esto creamos las instancias y las respectivas asignaciones:

¿Qué sucedería si en este punto establecemos el siguiente orden de instrucciones?

La salida en pantalla sería:

Lo que sucede es que la instancia masterCard como depende de la instancia gochi mediante la referencia fuerte que existe entre las clases Person y CreditCard. Aún cuando al objeto de tipo CreditCard se igualó a nil antes que a Person, el segmento de memoria que corresponde a masterCard es retenido hasta que la instancia gochi es liberada. Tal y como se muestra en el orden de las líneas 3 y 5 de la salida en pantalla.

El diagrama de esta nueva configuración sería el siguiente:

Relacion Correcta - Person y Creditcard

Antes de finalizar os dejo una pequeña tabla donde podemos consultar las características de las las referencias que hemos visto en este artículo:

ReferenciaVarLetOpcionalNo Opcional
Strong
WeakNoNo
UnownedNo

En este punto creo que es más que evidente la importancia de una buena gestión de la memoria, entender el funcionamiento de ARC, así como dominar los distintos tipos de referencias con los que contamos en Swift. Sin dudas este conocimiento es fundamental en la formación como programadores Swift, nos ayudará a implementar un mejor código y con esto lograr un producto final de muy alta calidad.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

Cualquier sugerencia, ya sea errores a corregir, información o ejemplos a añadir será, más que bienvenida, necesaria!