Referencias Strong, weak y unowned: ARC en acción

ARC strong weak unowned

A menudo solemos entrar en este tema cuando vemos en el debugger que nuestra aplicación está consumiendo una determinada cantidad de memoria y la misma nunca es liberada.

Básicamente, en la mayoría de los casos, se trata de referencias cíclicas entre clases que no le permiten al ARC poder liberarlas de la memoria. Veamos de qué se trata.

ARC: Automatic Reference Counting (Contador de referencias automático)

El contador de referencias automático es una funcionalidad que permite liberar la memoria de aquellos elementos que no posean referencias fuertes hacia ellos (strong references).

Esto es, cada vez que creamos una nueva referencia a una clase, ARC reserva un espacio en memoria para poder guardar el tipo de instancia y los valores de cada una de las propiedades del objeto.

En el momento en que esa referencia es nil y no existe ninguna otra referencia hacia ese objeto, ARC libera la memoria que estaba siendo ocupaba para poder usar ese espacio para otros elementos.

Para estar seguros de que no se está liberando memoria que está siendo utilizada por alguna parte del código, ARC chequea que la cantidad de referencias strong hacia ese elemento sean 0.

Supongamos que tenemos esta clase y definimos 3 variables opcionales de ella:

[code lang=”Swift”]
class Cliente {
let nombre:String
init(nombre:String)
{
self.nombre = nombre
print(“Instancia cliente creada”)
}

deinit {
print(“Instancia borrada de la memoria”)
}
}

var cliente1: Cliente?
var cliente2: Cliente?
var cliente3: Cliente?
[/code]

Al tratarse de variables opcionales, las mismas son inicializadas en nil y no referencian a ningún objeto de la clase Cliente. A continuación, instanciamos la primera:

[code lang=”Swift”]
cliente1 = Cliente(nombre: “Gabriel”)
[/code]

En este momento, el sistema reserva memoria para alocar el objeto cliente1 (se imprime el mensaje “Instancia cliente creada”). Si a su vez, creamos otras dos referencias a este objeto logramos obtener 3 referencias hacia la misma posición de memoria:

[code lang=”Swift”]
cliente2 = cliente1
cliente3 = cliente1
[/code]

Esto implicaría que si se modificara el nombre de la referencia cliente2, por ejemplo, se afectaría también a cliente1 y cliente3.

A esta altura, lo que está sucediendo es que tenemos 3 referencias strong a la misma instancia Cliente. Por lo tanto, si una o dos de ellas se igualaran a nil, la instancia seguiría existiendo:

[code lang=”Swift”]
cliente1 = nil
cliente2 = nil
[/code]

Por este motivo, ARC no puede desasignar esa posición de memoria hasta tanto todas las referencias fuertes hacia ella se eliminen. Luego de ejecutarse la siguiente sentencia, se imprimiría por fin el mensaje “Instancia borrada de la memoria”:

[code lang=”Swift”]
cliente3 = nil
[/code]

El problema más común: referencias strong entre instancias

Como ya comentamos, una vez que ARC detecta que una instancia tiene 0 referencias strong, elimina ese contenido de la memoria para poder ser utilizada nuevamente.

El problema es que es factible escribir código en el cual se llegue a un punto en el que nunca sea posible llegar a 0 referencias strong, y por ese motivo esos elementos nunca se eliminen de la memoria.

Supongamos que tenemos dos clases como las siguientes y creamos una variable opcional de cada una de ellas:

[code lang=”Swift”]
class Persona {
let nombre:String

init(nombre:String)
{
self.nombre = nombre
print(“Instancia Persona creada”)
}

var vehiculo:Vehiculo?

deinit {
print(“Instancia Persona borrada de la memoria”)
}
}

class Vehiculo {
let tipo:String

init(tipo:String)
{
self.tipo = tipo
print(“Instancia Vehiculo creada”)
}

var dueño:Persona?

deinit {
print(“Instancia Vehiculo borrada de la memoria”)
}
}

var persona1:Persona?
var vehiculo1:Vehiculo?
[/code]

Como bien dijimos anteriormente, a esta altura son simplemente variables sin contenido ya que no están apuntando a ninguna posición de la memoria. En el momento en que las instanciamos:

[code lang=”Swift”]
persona1 = Persona(nombre: “Gabriel”)
vehiculo1 = Vehiculo(tipo: “Camioneta”)
[/code]

estaríamos teniendo lo siguiente:

Automatic Reference Counting

Es decir, cada variable estaría utilizando una referencia strong a la posición de memoria a la cual están apuntando.

En este momento, se pueden linkear cada una de las instancias entre sí de la siguiente manera; dado que las variables son opcionales, se pueden “desenvolver” (unwrap) utilizando el signo de exclamación:

[code lang=”Swift”]
persona1!.vehiculo = vehiculo1
vehiculo1!.dueño = persona1
[/code]

Así es como se vería lo que tenemos por ahora:

Automatic Reference Counting

Lo que está sucediendo en este instante es que cada instancia crea una referencia fuerte (strong) hacia la otra, logrando que cada una tenga un total de dos referencias strong.

Este es un escenario muy común en muchos diseños de clases y en muchas ocasiones es difícil para el desarrollador prever esta situación de antemano. El problema, es que cuando se rompen las referencias hacia esas posiciones de memoria, la suma de referencias strong no suman 0:

Automatic Reference Counting

Por lo tanto, esas posiciones de memoria quedan ocupadas y no puede liberarse el espacio.

¿Cómo resolver este problema entonces?

Para poder evitar el inconveniente de las referencias strong cíclicas, swift nos provee dos tipos más de referencias: weak y unowned. Ambas dos permiten apuntar a una instancia en memoria sin mantener una referencia fuerte hacia el mismo, lo que permite que ARC pueda liberar la memoria al momento de romper la única referencia strong que queda.

La diferencia que existe entre ellas es que weak es válido para aquellas variables que pueden ser nil en algún momento de la ejecución del programa (se utiliza con opcionales) y en cambio unowned, se utiliza para variables que siempre tienen un valor.

Resolviendo la problemática con weak

Volviendo al ejemplo anterior, teniendo en cuenta que un vehículo puede o no tener un dueño, se puede modificar la clase de la siguiente manera:

[code lang=”Swift”]
class Vehiculo {
let tipo:String

init(tipo:String)
{
self.tipo = tipo
print(“Instancia Vehiculo creada”)
}

weak var dueño:Persona?

deinit {
print(“Instancia Vehiculo borrada de la memoria”)
}
}
[/code]

En ese caso, la relación que estaríamos consiguiendo sería la siguiente:

Automatic Reference Counting

Por lo tanto, la variable persona1 tiene una sola referencia strong, ya que la segunda es weak. Al igualar la misma a nil estaríamos logrando que ARC libere la memoria ya que la sumatoria de referencias fuertes es igual a 0:

[code lang=”Swift”]
persona1 = nil
[/code]

En este momento, solo estaríamos teniendo la variable vehiculo1 con una sola referencia fuerte, ya que la otra fue eliminada por ARC en el paso anterior. De la misma manera, igualando ésta a nil estaríamos liberando la memoria completamente:

[code lang=”Swift”]
vehiculo1 = nil
[/code]

Esta solución es posible ya que se planteó que un vehículo puede o no tener un dueño, con lo cual la variable “dueño” es opcional (en alguna parte de su ciclo de vida puede valer nil).

Este concepto es muy importante ya que está relacionado con la forma en la que el sistema trata a estas referencias. Como las mismas no mantienen una referencia strong al elemento al que apuntan, es posible que esa instancia sea eliminada de la memoria mientras la referencia weak está apuntando al elemento en cuestión. Es en ese momento cuando ARC iguala a esa variable a nil, para poder liberar ese espacio. Por lo tanto, se debe tratar siempre de variables y no de constantes, ya que de lo contrario ARC nunca podría liberar la memoria.

Resolviendo la problemática con unowned

Al momento de definir el diagrama de clases es importante entender que, si se puede dar un caso de referencias cíclicas, hay que tener cuidado con las referencias strong. Ya vimos que si la variable puede llegar a ser nil en algún momento, entonces ella debe ser weak. Esto le va a permitir a ARC poder igualarlo a nil al momento de liberar el espacio en memoria. Sin embargo, hay situaciones en las cuales la referencia siempre va a tener un valor durante su ciclo de vida, y es en esas situaciones en las cuales podemos resolver este problema usando referencias unowned.

Suponiendo que el ejemplo anterior se utiliza para un sistema que permite llevar un inventario de corredores de carreras Formula 1, en donde cada piloto (clase Persona) siempre tiene un auto (clase Vehiculo) y a su vez el vehículo siempre tiene un piloto, la forma de moldear la clase sería la siguiente:

[code lang=”Swift”]
class Vehiculo {
let tipo:String

init(tipo:String, dueño:Persona)
{
self.tipo = tipo
self.dueño = dueño
print(“Instancia Vehiculo creada”)
}

unowned var dueño:Persona

deinit {
print(“Instancia Vehiculo borrada de la memoria”)
}
}
[/code]

Un dato a tener en cuenta es que, como la variable dueño siempre tiene que tener un valor (por ser declarada como unowned), no puede ser opcional. Por ese motivo, debe ser inicializada si o si dentro de init.

El resto de la operativa es idéntica a los ejemplos anteriores y la forma en la que se verían las relaciones tendrían este aspecto:

Automatic Reference Counting

Si quieres aprender más sobre que es iOS sigue este enlace con una guía definitiva sobre el mejor Sistema Operativo para móviles.

Conclusiones

  • Si no se especifica la referencia al crear una variable, la misma es strong
  • ARC libera la memoria cuando la cantidad de referencias strong es igual a 0
  • Para resolver el problema de las referencias cíclicas strong, se utilizan referencias weak y unowned
  • Weak se debe usar cuando el atributo puede valer nil en alguna parte de su ciclo de vida
  • Unowned se debe usar cuando está garantizado que ese atributo siempre tendrá un valor
Name
Email
Review Title
Rating
Review Content
Referencias Strong, weak y unowned: ARC en acción
5,0 rating based on 12.345 ratings
Overall rating: 5 out of 5 based on 9 reviews.