Alexandre Freire

Blog sobre desarrollo en Swift, iOS y Xcode

¿Qué es una referencia circular en Swift?

Feb 11, 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 B, y la clase B tiene, a su vez, una propiedad de tipo A.

Es mucho más sencillo de lo que suena.

Imagina que tenemos la misma clase Author del artículo anterior, con una nueva propiedad book, 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")
    }
}

Book es una nueva clase que tiene una propiedad title y una propiedad author, 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 Author? y otro de tipo Book? y ver cómo se comporta la memoria, y la cuenta de referencias (retainCount) 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 george y la instancia de tipo Author, y otra referencia fuerte entre el símbolo got y la instancia Book:

Es decir, el retainCount 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 Book, 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 nil la variable que los contiene. Esto hará que se rompa la referencia fuerte entre george y got con sus respectivas instancias; o lo que es lo mismo, el retainCount de cada una se reduce en 1:

george = nil
got = nil

Observa que ningún método deinit 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 nil 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 retainCount 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 retainCount es igual a 1) pero no podemos acceder a ellos, ya que los símbolos author y got 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 weak, o unowned.

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

Referencias weak

Una referencia weak o débil se indica escribiendo la palabra weak 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 nil el valor de la propiedad si la instancia a la que apunta ha sido eliminada de memoria. Por lo tanto, las propiedades weak 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 author de la clase Book:

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 author de la clase Book no incrementa el retainCount de la instancia Author, 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 george o got a nil? 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 retainCount de la instancia de Author 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 george a nil, no sólo se reduce en 1 la cuenta de referencias del objeto Author, si no también la del objeto Book:
ObjectretainCount
Author0
Book1

Si finalmente asignamos got a nil, 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 got a nil, en lugar de hacerlo con george, podemos observar que ningún método deinit 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 retainCount igual a uno.

ObjectretainCount
Author1
Book1

Si continuamos y asignamos george a nil:

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 deinit de las dos instancias, pues ambas terminan con un retainCount 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?
Share on Facebook
Facebook
Pin on Pinterest
Pinterest
Tweet about this on Twitter
Twitter
Share on LinkedIn
Linkedin
Buffer this page
Buffer
Share on Reddit
Reddit

2 Comentarios

  1. Cristian

    El contenido y la explicación es realmente increíble y tiene mucha calidad, hasta ahora es el único artículo que me hizo comprender el tema al 100% . Estaré al tanto de todo el contenido que subas. Felicitaciones 🙂

    Responder
    • Alexandre Freire

      Muchas gracias Cristian! En cuanto acabe la mudanza vuelvo a los posts semanales 🙂

      Responder

Enviar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *