Skip to content
ARC

¿Qué es una referencia circular en Swift?

11 febrero, 2019

En el artículo anterior hemos visto cómo funciona ARC (Automatic Reference Counting).

Es un mecanismo muy sencillo que nos permite olvidarnos casi por completo del manejo de memoria en Swift, como si de un recolector de basura se tratase.

Esto no es siempre así, y existen ocasiones en las que le tenemos que echar una mano. Una de ellas es cuando creamos una referencia circular fuerte entre dos clases.

¿Qué es una referencia circular fuerte?

Una referencia circular ocurre cuando una clase A tiene una propiedad de tipo <strong>B</strong>, y la clase <strong>B</strong> tiene, a su vez, una propiedad de tipo <strong>A</strong>.

Es mucho más sencillo de lo que suena.

Imagina que tenemos la misma clase <strong>Author</strong> del artículo anterior, con una nueva propiedad <strong>book</strong>, de tipo Book?, ya que no todos los autores tienen que haber escrito ya un libro.

class Author {
    
    // MARK: Properties
    let name: String
    var book: Book?
    
    // MARK: Initialization
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("Author called \(name) is being deinitialized")
    }
}

<strong>Book</strong> es una nueva clase que tiene una propiedad title y una propiedad <strong>author</strong>, de tipo Author? ya que pueden existir libros con autores anónimos.

class Book {
    
    // MARK: Properties
    let title: String
    var author: Author?
    
    // MARK: Initialization
    init(title: String) {
        self.title = title
    }
    
    deinit {
        print("Book called \(title) is being deinitialized")
    }
}

Con estas dos clases, estamos creando una referencia circular entre ellas:

Únete a la newsletter para no perderte ningún nuevo tutorial: recibe un email cuando se publiquen nuevos artículos o vídeos y no pierdas la oportunidad de seguir aprendiendo. 

¿Por qué ARC no es capaz de liberar la memoria cuando hay referencias circulares fuertes?

Porque la cuenta de referencias de las instancias nunca llega a cero.

Vamos a crear un objeto de tipo <strong>Author?</strong> y otro de tipo Book? y ver cómo se comporta la memoria, y la cuenta de referencias (<strong>retainCount</strong>) de cada una de las instancias.

Ronda 1: creamos las instancias

var george: Author? = Author(name: "George R. R. Martin")
var got: Book? = Book(title: "Game of Thrones")

En estos momentos, se ha creado una referencia fuerte entre el símbolo <strong>george</strong> y la instancia de tipo <strong>Author</strong>, y otra referencia fuerte entre el símbolo <strong>got</strong> y la instancia <strong>Book</strong>:

Es decir, el <strong>retainCount</strong> de cada instancia es igual a 1.

ObjectretainCount
Author1
Book1

Ronda 2: asignar propiedades

Vamos a hacer ahora la asignación de las propiedades opcionales de cada clase.

george?.book = got
got?.author = george 

Ahora mismo, en memoria tenemos esto:

Tal y como podemos observar en la imagen, la instancia Author guarda una referencia fuerte hacia la instancia <strong>Book</strong>, y viceversa. Ahora mismo, cada instancia tiene dos referencias fuertes apuntando hacia ellas:

ObjectretainCount
Author2
Book2

Ronda 3: fuga de memoria

En esta ronda vamos a decrementar el retainCount de cada instancia asignando a <strong>nil</strong> la variable que los contiene. Esto hará que se rompa la referencia fuerte entre <strong>george</strong> y <strong>got</strong> con sus respectivas instancias; o lo que es lo mismo, el <strong>retainCount</strong> de cada una se reduce en 1:

george = nil
got = nil

Observa que ningún método <strong>deinit</strong> ha sido ejecutado. Esto sucede porque la memoria utilizada para guardar el objeto todavía no ha sido liberada.

Antes, la tabla nos mostraba que cada instancia tenía un retainCount igual a 2. Después de asignar a <strong>nil</strong> las variables, se reduce en 1 en cada instancia, siendo ahora:

ObjectretainCount
Author1
Book1

La referencia fuerte que tiene cada instancia hacia la otra, evita que el <strong>retainCount</strong> llegue a cero. Por tanto, ARC no libera la memoria y…

… ¡ENHORABUENA! Acabas de crear una fuga de memoria (memory leak) en tu aplicación.

Una fuga de memoria ocurre cuando existe un objeto en memoria, pero éste es inaccesible. Tal y como ocurre en este caso, en el que los objetos siguen existiendo en memoria (su <strong>retainCount</strong> es igual a 1) pero no podemos acceder a ellos, ya que los símbolos <strong>author</strong> y <strong>got</strong> ya no apuntan hacia ellos. 

¿Cómo se soluciona una referencia circular entre dos clases?

Sólo tenemos que romper la referencia circular.

¿Cómo?, te estaras preguntando…

Tenemos que transformar una de las referencias fuertes en una referencia débil.

¿Qué es una referencia débil y cómo se crea?

Una referencia débil es aquélla que no incrementa en +1 la cuenta de referencias de la instancia, permitiendo que ARC libere la memoria correctamente.

Se pueden crear de dos maneras: con la palabras reservadas <strong>weak</strong>, o <strong>unowned</strong>.

En este artículo vamos a ver cómo solucionar una referencia circular utilizando <strong>weak</strong>. En el siguiente artículo veremos la diferencia que existe entre <strong>weak</strong> y <strong>unowned</strong>.

Referencias weak

Una referencia weak o débil se indica escribiendo la palabra <strong>weak</strong> delante de la declaración de una propiedad o variable.

Esto hará que no se sume +1 a la cuenta de referencias de esa instancia y que ARC pueda liberar la memoria cuando sea necesario. Además, ARC asignará a <strong>nil</strong> el valor de la propiedad si la instancia a la que apunta ha sido eliminada de memoria. Por lo tanto, las propiedades <strong>weak</strong> tienen que declararse de tipo opcional (esto lo entenderemos mejor con el siguiente ejemplo).

Vamos a solucionar la referencia circular de nuestro ejemplo transformando una de las propiedades en weak. En este caso, lo haremos con la propiedad <strong>author</strong> de la clase <strong>Book</strong>:

class Book {
    
    // MARK: Properties
    let title: String
    weak var author: Author? // <- WEAK!
    
    // MARK: Initialization
    init(title: String) {
        self.title = title
    }
    
    deinit {
        print("Book called \(title) is being deinitialized")
    }
}

Vayámonos directamente al punto de la Ronda 2. Después de asignar las propiedades, tendremos este layout en memoria:

La propiedad <strong>author</strong> de la clase <strong>Book</strong> no incrementa el <strong>retainCount</strong> de la instancia <strong>Author</strong>, por lo que la traza queda así:

ObjectretainCount
Author1
Book2

La instancia de tipo Book sigue teniendo dos referencias fuertes apuntando hacia ella, pero la de tipo Author solamente tiene una.

¿Qué pasaría ahora si asignamos las variables <strong>george</strong> o <strong>got</strong> a <strong>nil</strong>? Lo vemos en la Ronda 3. Para ello, vamos a seguir dos caminos:

  • Asignando la variable george a nil en primer lugar.
  • Asignando la variable got a nil en primer lugar.

Veremos que la memoria se libera de manera diferente en ambos casos.

Ronda 3.1: Asignamos george a nil:

Si asignamos george a nil, eliminamos la referencia fuerte existente entre el símbolo george y la instancia Author, reduciendo en 1 su cuenta de referencias (retainCount).

george = nil 

// Se imprime por consola:
// "Author called George R. R. Martin is being deinitialized"

El <strong>retainCount</strong> de la instancia de <strong>Author</strong> llega a cero porque no hay más referencias fuertes apuntando hacia ella, y por lo tanto ARC la elimina de memoria.

Fíjate en dos cosas:

  1. La propiedad author del objeto Book se asigna a nil, ya que la instancia ha sido eliminada de memoria (por eso tiene que ser opcional)
  2. Cuando una instancia es eliminada de memoria, también se eliminan las referencias que ésta tenga hacia otras instancias. Esto quiere decir que, al asignar <strong>george</strong> a <strong>nil</strong>, no sólo se reduce en 1 la cuenta de referencias del objeto <strong>Author</strong>, si no también la del objeto <strong>Book</strong>:
ObjectretainCount
Author0
Book1

Si finalmente asignamos <strong>got</strong> a <strong>nil</strong>, su cuenta de referencias llega a cero y se libera la memoria correctamente (no se produce ninguna fuga de memoria o memory leak)

got = nil 
// Se imprime por consola: 
// "Book called Game of Thrones is being deinitialized"

Ronda 3.2: Asignamos got a nil:

Si comenzamos asignando <strong>got</strong> a <strong>nil</strong>, en lugar de hacerlo con <strong>george</strong>, podemos observar que ningún método <strong>deinit</strong> se ejecuta tras esta linea:

got = nil

¿Por qué ocurre esto?

Esto se debe a que cada instancia todavía tiene una referencia fuerte apuntando hacia ella, o lo que es lo mismo, ambas tienen un <strong>retainCount</strong> igual a uno.

ObjectretainCount
Author1
Book1

Si continuamos y asignamos <strong>george</strong> a <strong>nil</strong>:

george = nil 

// Se imprime por consola:
// "Author called George R. R. Martin is bein deinitialized"
// y también 
// "Book called Game of Thrones is being deinitialized"

Se ejecutan los métodos <strong>deinit</strong> de las dos instancias, pues ambas terminan con un <strong>retainCount</strong> igual a cero.

Conclusión

La llegada de ARC supuso un gran avance para el desarrollo para plataformas Apple. Atrás quedaron los tiempos en los que los desarrolladores tenían que llevar la cuenta de referencias manualmente, y llamar a la función release para que se liberase la memoria.

Gracias a ARC podemos olvidarnos, en la mayoría de las ocasiones, que Swift no dispone de un recolector de basura. Sin embargo, se nos tiene que encender una luz roja cuando trabajamos con tipos por referencia, como clases o clousures, ya que es posible crear referencias circulares y provocar fugas de memoria.


¿Me ayudas a compartir en redes sociales?