Alexandre Freire

Blog sobre desarrollo en Swift, iOS y Xcode

Referencias circulares en Closures

Feb 20, 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 (self.someProperty), o si se llama a algún método de la instancia (self.someMethod())

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

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

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 Person? y utilizar el closure.

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

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

Por último, vamos a asignar person a nil.

person = nil

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

¿Qué está pasando?

La clase Person 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 self.firstName y self.lastName).

En estos casos, se dice que captura a self.

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

ObjectretainCount
Person2
Closure () -> String1

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.

ObjectretainCount
Person1
Closure () -> String1

¿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 weak o como unowned, 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 unowned y weak.

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

En nuestro caso, vamos a utilizar unowned 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 weak en lugar de unowned, self sería de tipo opcional dentro del closure. Recuerda que toda propiedad weak debe ser declarado de tipo Opcional, ya que puede llegar a valer nil 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 self 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 self 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 self 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 weak o unowned delante del nombre de la instancia que van a capturar.

Define una capture list como unowned 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 weak cuando la instancia capturada pueda convertirse en nil en cualquier momento futuro. Todas las referencias weak son de tipo Opcional, y automáticamente pasarán a valer nil 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?
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

0 comentarios

Enviar un comentario

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