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.
Object | retainCount |
Author | 1 |
Book | 1 |
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:
Object | retainCount |
Author | 2 |
Book | 2 |
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:
Object | retainCount |
Author | 1 |
Book | 1 |
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í:
Object | retainCount |
Author | 1 |
Book | 2 |
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
anil
en primer lugar. - Asignando la variable
got
anil
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:
- La propiedad
author
del objetoBook
se asigna anil
, ya que la instancia ha sido eliminada de memoria (por eso tiene que ser opcional) - 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>
:
Object | retainCount |
Author | 0 |
Book | 1 |
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.
Object | retainCount |
Author | 1 |
Book | 1 |
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.