Skip to content
ARC

Referencias circulares en Closures

20 febrero, 2019

Bienvenido a la serie sobre Gestión de Memoria en Swift. Este será el cuarto episodio. Puedes ver los anteriores aquí:

¿Sabías que si estás utilizando closures podrías estar plagando tu aplicación de fugas de memoria?

Los closures son Reference-Types

Pues sí… Al igual que las clases, los closures son tipos por referencia, por lo que si asignas uno a una variable o propiedad, lo que haces es asignar una referencia fuerte a ese closure.

Por lo tanto, una referencia circular fuerte puede ocurrir también si asignas un closure a una propiedad de una instancia de una clase, y el cuerpo (body) del closure captura la propia instancia. Puede ocurrir si dentro del body del closure se accede a alguna propiedad de la instancia (<strong>self.someProperty</strong>), o si se llama a algún método de la instancia (<strong>self.someMethod()</strong>)

¿Cómo se forma una referencia circular en un closure?

Vamos a definir una clase <strong>Person</strong> que tenga una <strong>firstName</strong> y <strong>lastName</strong>, para guardar su nombre y apellidos. Al mismo tiempo, <strong>Person</strong> tiene una propiedad <strong>lazy</strong> llamada <strong>fullName</strong>. Esta propiedad referencia a un closure que crea el nombre completo de la persona mediante su nombre y su apellido. Es de tipo <strong>() -> String</strong>, o lo que es lo mismo, «una función que no acepta ningún parámetro y devuelve un <strong>String</strong>«.

class Person {
    
    // MARK: Properties
    let firstName: String
    let lastName: String
    
    lazy var fullName: () -> String = {
        return "\(self.firstName) \(self.lastName)"
    }
    
    // MARK: Initialization
    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
    
    deinit {
        print("Person called \(firstName) is being deinitialized")
    }
}

A continuación vamos a crear una instancia de tipo <strong>Person?</strong> y utilizar el closure.

var person: Person? = Person(firstName: "Tyrion", lastName: "Lannister")

print(person?.fullName()) // "Tyrion Lannister"

Por último, vamos a asignar <strong>person</strong> a <strong>nil</strong>.

person = nil

Vemos que no se ejecuta el método <strong>deinit</strong>, por lo que estamos creando un retain cycle (referencia circular) y una fuga de memoria.

¿Qué está pasando?

La clase <strong>Person</strong> tiene una referencia fuerte hacia el closure, y éste tiene otra referencia fuerte hacia la instancia porque está capturándola, ya que hace referencia a self dentro de su body (mediante <strong>self.firstName</strong> y <strong>self.lastName</strong>).

En estos casos, se dice que captura a <strong>self</strong>.

Esto hace que el <strong>retainCount</strong> de <strong>Person</strong> nunca llegue a cero y que no se elimine de memoria.

ObjectretainCount
Person2
Closure <strong>() -> String</strong>1

La solución pasa por transformar la referencia fuerte que tiene el closure hacia la instancia en una referencia débil que no incremente la cuenta de referencias.

Object<strong>retainCount</strong>
Person1
Closure <strong>() -> String</strong>1

¿Cómo lo solucionamos?

Swift nos da una técnica muy elegante para resolver una referencia circular fuerte dentro de un closure, conocida como closure capture list.

Una capture list define las reglas que se van a utilizar al capturar un tipo por referencia dentro del body del closure. Se puede declarar cada referencia capturada como <strong>weak</strong> o como <strong>unowned</strong>, al igual que sucedía en las referencias circulares entre dos clases. La elección de una u otra depende de las relaciones entre las diferentes partes de tu código. Échale un vistazo al episodio 3 para recordar las diferencias entre <strong>unowned</strong> y <strong>weak</strong>.

Para crear una capture list, simplemente ponemos entre corchetes <strong>unowned</strong> o <strong>weak</strong> seguido del nombre de la referencia que vamos a capturar. Por ejemplo, <strong>[weak self] </strong>ó <strong>[unowned self]</strong>

En nuestro caso, vamos a utilizar <strong>unowned</strong> puesto que la instancia y el closure tienen el mismo ciclo de vida.

lazy var fullName: () -> String = { [unowned self] in 
    return "\(self.firstName) \(self.lastName)"
}

Sólamente con este cambio, si volvemos a ejecutar el Playground vemos que se libera correctamente la memoria y ya no se producen memory leaks.

Si hubiésemos escogido <strong>weak</strong> en lugar de <strong>unowned</strong>, <strong>self</strong> sería de tipo opcional dentro del closure. Recuerda que toda propiedad <strong>weak</strong> debe ser declarado de tipo Opcional, ya que puede llegar a valer <strong>nil</strong> en algún momento.

Ú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. 

El compilador de Swift, nuestro mejor aliado

Observa que, siempre que intentas acceder a <strong>self</strong> dentro del body de un closure, el compilador nos lanza un error: «Reference to property ‘someProperty’ in closure requires explicit ‘self.’ to make capture semantics explicit». Y nos propone un quick-fix que consiste en añadir <strong>self</strong> delante de la propiedad o el método que estemos utilizando.

Te puedes tomar esto como una especie de aviso o de warning que te lanza el compilador para recordarte que, al estar utilizando <strong>self</strong> dentro del closure, puedes estar creando una referencia circular. Algo así como: «¡Ey! Que sepas que estás utilizando self aquí dentro. Tú verás si quieres utilizar un capture list»

Conclusión

Los closures son, al igual que las clases, tipos por referencia en Swift. Por ello, tienen el potencial de provocar referencias circulares y fugas de memoria.

La solución pasa por crear un capture list dentro del body del closure. Se pueden crear poniendo <strong>weak</strong> o <strong>unowned</strong> delante del nombre de la instancia que van a capturar.

Define una capture list como <strong>unowned</strong> cuando el closure y la instancia que captura siempre se refieran la una a la otra (tengan el mismo ciclo de vida) y por lo tanto siempre se liberarán de memoria al mismo tiempo.

Utiliza <strong>weak</strong> cuando la instancia capturada pueda convertirse en <strong>nil</strong> en cualquier momento futuro. Todas las referencias <strong>weak</strong> son de tipo Opcional, y automáticamente pasarán a valer <strong>nil</strong> cuando la instancia a la que se refiere es liberada de memoria. En estos casos, comprueba dentro del body si todavía tiene valor.

Si te ha gustado lo que has leído, me ayudaría que lo compartieses en tus redes. También te invito a dejarme feedback en los comentarios o me cuentes sobre qué temas te gustaría que escribiese en el futuro.


¿Me ayudas a compartir en redes sociales?