Automatic Reference Counting (ARC) en Swift en Español
ARC en Swift

Automatic Reference Counting (ARC) en Swift en Español

ARC en Swift. Igual que con otros lenguajes de programación, la memoria que usa nuestra aplicación es importante, ya que la memoria es un recurso limitado. Con este post vamos a ver cómo funciona ARC con ejemplos prácticos y cómo podemos resolver algunos problemas.

SwiftBeta

Swift usa Automatic Reference Counting (ARC) para trackear y manejar el uso de memoria en tu app. ARC automáticamente libera la memoria usada por las instancias de tus clases cuando ya no son necesarias.

Pero, en algunos casos ARC requiere más información sobre las relaciones que creas en tu código para que siga haciendo todo el trabajo por ti.

Esto solo afecta a instancias de clases. Recordamos que las struct y los enum son reference types, no son reference types y por lo tanto no se pasan por referencia.


¿Cómo funciona ARC?

Cada vez que instancias una clase, ARC asigna un espacio de memoria para guarda la información sobre la instancia. En ese espacio de memoria se guarda información sobre el tipo de la instancia, junto con los valores de cada propiedad asociada a la instancia.

Adicionalmente, cuando una instancia ya no es necesaria, ARC libera la memoria usada por esa instancia y así puede usarse por otra instancia en el futuro. La memoria del device es finita, es por eso que tenemos que ir liberándola.

Sin embargo, si ARC libera una instancia que aún está en uso, no sería posible acceder a las propiedades de la instancia o métodos. De hecho, si tratas de acceder y la instancia no existe en memoria, la app crasheará.

Para asegurarse que las instancia no desaparecen mientras están siendo usadas, ARC trackea cuantas propiedades, constantes y variables tienen una referencia de esa instancia. Es decir, ARC no liberará una instancia si hay alguna referencia activa que aún existe.

Para hacer esto posible, cuando asignamos a una propiedad una instancia de una clase, la referencia es strong (referencia fuerte) y si tenemos una instancia A que tiene una referencia a una instancia B y viceversa, se generaría un retain cycle es decir, esas dos instancias estarían activas en memoria hasta que la app finalizara (force quit del usuario, crash, etc).


Ejemplos

Vamos a ver un ejemplo de como funciona ARC. Empezamos con una clase sencilla, que contiene una constante como propiedad llamada name

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

Hemos añadido un print cuando inicializamos la instancia y otro cuando se libera.

El siguiente código muestra 3 referencias del tipo Person? y vamos a inicializar la primera con una instancia de Person

var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "SwiftBeta")

// El resultado que aparece es "SwiftBeta is being initialized"

Como la nueva instancia de Person ha sido asignada a reference1 se ha creado una referencia fuerte de reference1 a la instancia de Person.

Como tenemos al menos una referencia fuerte, ARC se asegura de que la instancia de Person permanezca en memoria y no es liberado.

Si asignamos la misma instancia de Person a más variables, crearemos más referencias fuertes a esa instancia.

reference2 = reference1
reference3 = reference1

Ahora mismo hay 3 referencias fuertes a la única instancia de Person que hemos creado.

Para poder liberar la instancia de Person, debemos asignar nil a las 3 variables. Así ninguna de ellas tiene una referencia fuerte. Al hacerlo, se llamará el método del deinit y veremos el mensaje en la consola.

reference1 = nil
reference2 = nil
reference3 = nil

// El resultado que aparece es "SwiftBeta is being deinitialized"

Strong References Cycles entre instancias de clases

El ejemplo anterior ARC es capaz de trackear el número de referencias que tiene la instancia de Person, y así liberar la instancia cuando es conveniente.

Sin embargo, es posible crear código donde las instancias de clase nunca llegan a tener 0 referencias fuertes. Esto pasa si dos instancias de clase tienen referencias fuertes entra las dos. Es decir, la instancia de A tiene una referencia fuerte a la instancia de B y la instancia B tiene referencia fuerte a A.

Para poder resolver estas referencias fuertes, debes cambiar alguna de las relaciones entre tus instancias de clases con weak o unowned en lugar de crear una referencia fuerte.

Vamos a ver como se crean estos retain cycles con un ejemplo:

class PersonWithApartment {
    let name: String
    var apartment: Apartment?
    
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let unit: String
    var tenant: PersonWithApartment?
    
    init(unit: String) {
        self.unit = unit
        print("\(unit) is being initialized")
    }
    
    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

var john: PersonWithApartment?
var unit4A: Apartment?

john = PersonWithApartment(name: "John Applessed")
unit4A = Apartment(unit: "4A")


// El resultado que aparece es
"John Applessed is being initialized"
"4A is being initialized"

Hemos creado dos instancias, una de Person y la otra de Apartament. Ahora mismo la instancia de Person tiene una referencia fuerte de john, y la instancia de Apartment tiene una referencia fuerte de unit4A.

Hasta aquí todo bien, pero vamos a asignar un apartamento a john y un tenant a unit4A.

john!.apartment = unit4A
unit4A!.tenant = john

Al hacer esto, creamos referencias fuertes entre las dos instancias, y por lo tanto creamos un retain cycle.

La instancia de Person tiene una referencia fuerte a Apartment, y Apartment tiene una referencia fuerte a Person

Por lo tanto, cuando rompemos las referencia fuerte que tienen las variables john y unit4A, el número de referencias nunca llega a 0 y las instancias no son liberadas por ARC.

Es decir, al asignar nil a las variables john y unit4A el print que hemos puesto en el deinit nunca se llama.

john = nil
unit4A = nil

Así es como quedarían las referencias:


Resolviendo Ciclos de Strong References entre instancias de clase

Para solucionar el problema que hemos visto en la sección anterior, Swift nos provee de dos tipos de referencias: weak references (referencias débiles) o unowned references. Para especificar como queremos la referencia, tenemos dos keywords que ponemos de la siguiente manera:

weak var tenant: Person?

// o

unowned var tenant: Person?

Al usar una referencia débil no se crea una referencia fuerte a la instancia a la que apunta, es por eso que podemos liberar la instancia aún cuando la referencia débil esté presente.
Automáticamente, ARC setea la referencia débil a nil cuando la instancia a que apunta es nil.

Las referencias weak necesitan poder cambiar su valor a nil en runtime, es por eso que siempre son variables opcionales y no constantes.
Las Property Observers no son llamadas cuando ARC settea la referencia débil a nil.
class PersonWithApartmentAndWeak {
    let name: String
    var apartment: ApartmentAndWeak?
    
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class ApartmentAndWeak {
    let unit: String
    weak var tenant: PersonWithApartmentAndWeak?
    
    init(unit: String) {
        self.unit = unit
        print("\(unit) is being initialized")
    }
    
    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

var johnWeak: PersonWithApartmentAndWeak?
var unit4AWeak: ApartmentAndWeak?

johnWeak = PersonWithApartmentAndWeak(name: "John Applessed")
unit4AWeak = ApartmentAndWeak(unit: "4A")

johnWeak!.apartment = unit4AWeak
unit4AWeak!.tenant = johnWeak

johnWeak = nil
unit4AWeak = nil


// El resultado es

"John Applessed is being initialized"
"4A is being initialized"

"John Applessed is being deinitialized"
"Apartment 4A is being deinitialized"

Ahora las dos instancias son liberadas cuando no tenemos ninguna referencia hacía ellas. Es el mismo código que en el ejemplo anterior, pero solo hemos añadido la keyword weak.

La instancia de Person tiene una referencia fuerte a la instancia de Apartment, pero la instancia de Apartment ahora tiene una referencia débil a la instancia de Person. Esto significa que cuando rompes la referencia fuerte que tiene la variable johnWeak, no hay ninguna otra referencia fuerte a johnWeak.

johnWeak = nil

// El resultado es "John Applessed is being deinitialized"

Al hacerlo, liberamos la instancia de Person y la propiedad tenant de Apartment es seteada a nil. Lo vemos mejor en la siguiente imagen:

Y si seteamos unit4A a nil la instancia de Apartment es liberada, ya que no tiene ninguna referencia fuerte.

unit4AWeak = nil

El resultado sería:

En este post nos hemos centrado en cómo se crean retain cycles en nuestro código y a como arreglarlo. En otros posts veremos la otra keyword que hemos mencionado unowned y veremos también como con los closures podemos tener retain cycles.


Hasta aquí el post de hoy, gracias por leernos! 🤓
👉👉 SUSCRÍBETE al CANAL de youtube
Si tienes preguntas no dudes en contactar con nosotros a través de Twitter

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple


Swift Introducción