Skip to content

Recuento Automático de Referencias (ARC)

Posted on:15 de febrero de 2023

Table of contents

Open Table of contents

Introducción

Swift utiliza el Recuento Automático de Referencias (ARC, por sus siglas en inglés) para rastrear instancias y gestionar el uso de memoria. ARC libera automáticamente la memoria utilizada por las instancias de clase cuando ya no son necesarias. Esto significa que, en la mayoría de los casos la gestión de memoria “simplemente funciona” en Swift y no necesitas preocuparte por ella.

Sin embargo, en algunos casos, ARC requiere más información sobre las relaciones entre las partes de tu código para gestionar la memoria por ti. Este artículo describe esas situaciones y muestra cómo habilitar a ARC para gestionar la memoria de tu aplicación.

ARC solo se aplica a las instancias de clases. Las estructuras y enumeraciones son tipos de valor, no tipos de referencia, y no se almacenan ni se pasan por referencia.

¿Cómo funciona ARC?

Cada vez que creas una nueva instancia de una clase, ARC asigna un bloque de memoria para almacenar información sobre esa instancia. Esta memoria contiene información sobre el tipo de la instancia, junto con los valores de cualquier propiedad almacenada asociada a esa instancia.

Además, cuando una instancia ya no es necesaria, ARC libera la memoria utilizada por esa instancia para que la memoria pueda ser utilizada para otros propósitos. Esto garantiza que las instancias de clase no ocupen espacio en la memoria cuando ya no son necesarias.

Sin embargo, si ARC liberara una instancia que aún está en uso, ya no sería posible acceder a las propiedades de esa instancia o llamar a los métodos de esa instancia. De hecho, si intentaras acceder a la instancia, es probable que tu aplicación se bloquee.

Para asegurarte de que las instancias no desaparezcan mientras aún son necesarias, ARC rastrea cuántas propiedades, constantes y variables actualmente están haciendo referencia a cada instancia de clase. ARC no liberará una instancia mientras al menos una referencia activa a esa instancia todavía exista.

Para hacer esto posible, cada vez que asignas una instancia de clase a una propiedad, constante o variable, esa propiedad, constante o variable hace una referencia fuerte a la instancia. La referencia se llama una “referencia fuerte” porque mantiene una fuerte relación con esa instancia y no permite que se libere mientras esa referencia fuerte persiste.

ARC en acción

class Persona {
    let nombre: String // Atributo que almacena el nombre de la persona

    // Constructor que inicializa el nombre de la persona
    init(nombre: String) {
        self.nombre = nombre
        print("\(nombre) se está inicializando")
    }

    // Destructor que se ejecuta cuando la instancia de la clase es eliminada de la memoria
    deinit {
        print("\(nombre) se está desinicializando")
    }
}

// Variables opcionales que puede contener referencias a una instancia de Persona
var referencia1: Persona?
var referencia2: Persona?
var referencia3: Persona?


// Creamos una instancia de Persona con nombre "Juan Pérez" y asigna su referencia a la variable referencia1.
referencia1 = Persona(nombre: "Juan Pérez")
// Imprime "Juan Pérez se está inicializando"

//Asignamos la misma referencia a las variables referencia2 y referencia3
referencia2 = referencia1
referencia3 = referencia1

referencia1 = nil // Eliminamos la referencia a la instancia de Persona asociada a referencia1
referencia2 = nil // Eliminamos la referencia a la instancia de Persona asociada a referencia2

// Eliminamos la referencia a la instancia de Persona asociada a referencia3 y en este momento es cuando ARC detecta que no hay mas referencias a la instancia de "Juan Pérez", llama al "desinicializandor" y posteriormente libera la memoria que ocupaba esa instancia
referencia3 = nil
// Imprime "Juan Pérez se está desinicializando"

Ciclos de Referencias Fuertes entre Instancias de Clase

En el ejemplo anterior, ARC es capaz de rastrear el número de referencias a la nueva instancia de Persona que se crea y liberar esa instancia de Persona cuando ya no es necesaria.

Sin embargo, es posible escribir código en el que una instancia de clase nunca llegue a un punto en el que tenga cero referencias fuertes. Esto puede suceder si dos instancias de clase mantienen una referencia fuerte entre sí, de modo que cada instancia mantiene viva a la otra. Esto se conoce como un ciclo de referencia fuerte.

Aquí hay un ejemplo de cómo se puede crear accidentalmente un ciclo de referencia fuerte. Este ejemplo define dos clases llamadas Persona y Apartamento, que modelan un bloque de apartamentos y sus residentes:

class Persona {
    let nombre: String
    init(nombre: String) { self.nombre = nombre }
    var apartamento: Apartamento? // Referencia Fuerte de tipo "Apartamento"
    deinit { print("\(nombre) está siendo liberado de la memoria") }
}

class Apartamento {
    let unidad: String
    init(unidad: String) { self.unidad = unidad }
    var inquilino: Persona? // Referencia Fuerte de tipo "Persona"
    deinit { print("El apartamento \(unidad) está siendo liberado de la memoria") }
}

// Crea dos variables opcionales para una persona y un apartamento
var juan: Persona?
var unidad8B: Apartamento?

// Instancia una nueva persona y un nuevo apartamento
juan = Persona(nombre: "Juan Pérez")
unidad8B = Apartamento(unidad: "8B")

// Establece la propiedad de apartamento de Juan como el apartamento 8B y la propiedad de inquilino de 8B como Juan
juan!.apartamento = unidad8B
unidad8B!.inquilino = juan

// Libera los objetos desreferenciando las dos variables, pero las referencias fuertes dentro de las insancias no pueden ser liberadas por que se referencian la una a la otra
juan = nil
unidad8B = nil

Desafortunadamente, enlazar estas dos instancias crea un ciclo de referencia fuerte entre ellas. La instancia de Persona ahora tiene una referencia fuerte a la instancia de Apartamento, y la instancia de Apartamento tiene una referencia fuerte a la instancia de Persona. Por lo tanto, cuando rompes las referencias fuertes mantenidas por las variables juan y unidad8B, los recuentos de referencias no se reducen a cero, y las instancias no son liberadas por ARC.

Ten en cuenta que ninguno de los desinicializadores fue llamado cuando estableciste estas dos variables en nil. El ciclo de referencia fuerte evita que las instancias de Persona y Apartamento sean liberadas, lo que causa una fuga de memoria en tu aplicación.

Resolviendo Ciclos de Referencias Fuertes entre Instancias de Clase

Swift proporciona dos formas de resolver ciclos de referencias fuertes cuando trabajas con propiedades de tipo clase: referencias débiles (weak) y referencias sin propiedad (unowned).

Las referencias weak y unowned permiten que una instancia en un ciclo de referencia se refiera a la otra instancia sin mantener un control fuerte sobre ella. Las instancias pueden entonces referirse entre sí sin crear un ciclo de referencia fuerte.

Utiliza una referencia weak cuando la otra instancia tiene una duración más corta, es decir, cuando la otra instancia puede ser desasignada primero. En el ejemplo del apartamento anterior, es apropiado que un apartamento pueda no tener un inquilino en algún momento de su duración de vida, por lo que una referencia weak es una forma adecuada de romper el ciclo de referencia en este caso. En contraste, utiliza una referencia unowned cuando la otra instancia tiene la misma duración de vida o una duración de vida más larga.

Referencias Débiles (weak)

Una referencia débil (weak) es una referencia que no mantiene un control fuerte sobre la instancia a la que se refiere, por lo que no impide que ARC disponga de la instancia referenciada. Este comportamiento evita que la referencia se convierta en parte de un ciclo de referencias fuertes. Indicas una referencia weak colocando la palabra clave weak antes de la declaración de una propiedad o variable.

Debido a que una referencia weak no mantiene un control fuerte sobre la instancia a la que se refiere, es posible que esa instancia sea desasignada mientras la referencia weak aún se está refiriendo a ella. Por lo tanto, ARC establece automáticamente una referencia weak en nil cuando la instancia a la que se refiere es desasignada. Y, como las referencias débiles (weak) necesitan permitir que su valor se cambie a nil en tiempo de ejecución, siempre se declaran como variables, en lugar de constantes y de un tipo opcional.

Una referencia weak (weak var) siempre debe de ser opcional.

Puedes comprobar la existencia de un valor en la referencia weak, al igual que cualquier otro valor opcional, y nunca terminarás con una referencia a una instancia inválida que ya no existe.

Los observadores de propiedad no se llaman cuando ARC establece una referencia weak en nulo.

class Persona {
    let nombre: String
    init(nombre: String) { self.nombre = nombre }
    var apartamento: Apartamento? // Referencia Fuerte de tipo "Apartamento"
    deinit { print("\(nombre) está siendo liberado de la memoria") }
}

class Apartamento {
    let unidad: String
    init(unidad: String) { self.unidad = unidad }
    weak var inquilino: Persona? // Referencia Débil de tipo "Persona" <-----
    deinit { print("El apartamento \(unidad) está siendo liberado de la memoria") }
}

var juan: Persona?
var unidad8B: Apartamento?

juan = Persona(nombre: "Juan Pérez")
unidad8B = Apartamento(unidad: "8B")

juan!.apartamento = unidad8B
unidad8B!.inquilino = juan

juan = nil
// Juan Pérez está siendo liberado de la memoria
unidad8B = nil
// El apartamento 8B está siendo liberado de la memoria

Cambiando la variable inquilino a referencia weak podemos solucionar el ciclo de referencias fuertes que daba problemas anteriormente.

Referencias sin propiedad (unowned)

Al igual que una referencia weak, una referencia unowned no mantiene un fuerte control sobre la instancia a la que se refiere. Sin embargo, a diferencia de una referencia weak, se utiliza una referencia unowned cuando la otra instancia tiene la misma duración de vida o una duración de vida más larga. Indicas una referencia unowned colocando la palabra clave “unowned” antes de la declaración de una propiedad o variable.

A diferencia de una referencia weak, se espera que una referencia unowned siempre tenga un valor. Como resultado, marcar un valor como “unowned” no lo hace opcional y ARC nunca establece el valor de una referencia unowned a nulo.

Utiliza una referencia unowned solo cuando estés seguro de que la referencia siempre apunta a una instancia que no ha sido desasignada. Si intentas acceder al valor de una referencia unowned después de que esa instancia haya sido desasignada, obtendrá un error en tiempo de ejecución.

En el siguiente ejemplo se definen dos clases, Cliente y TarjetaDeCredito, que están relacionadas entre sí. Un cliente puede tener una tarjeta de crédito, pero no es obligatorio, mientras que una tarjeta de crédito siempre está asociada a un cliente.

Para evitar un ciclo de referencia fuerte, la clase TarjetaDeCredito tiene una propiedad de cliente unowned y no opcional, mientras que la clase Cliente tiene una propiedad de tarjeta opcional. Además, solo se puede crear una nueva instancia de TarjetaDeCredito pasando un valor de número y una instancia de cliente a un inicializador personalizado de TarjetaDeCredito. Esto asegura que cada instancia de TarjetaDeCredito tenga una instancia de cliente asociada con ella desde el momento de su creación.

class Cliente {
    let nombre: String
    
    // Define la propiedad tarjeta como una referencia opcional fuerte a una instancia de TarjetaDeCredito
    var tarjeta: TarjetaDeCredito?
    
    init(nombre: String) {
        self.nombre = nombre
    }
    
    // Define el destructor de Cliente
    deinit { print("(nombre) se está eliminando de la memoria") }
}

class TarjetaDeCredito {
    let numero: UInt64
    
    // Define la propiedad cliente como una referencia unowned a una instancia de Cliente
    unowned let cliente: Cliente
    
    init(numero: UInt64, cliente: Cliente) {
        self.numero = numero
        self.cliente = cliente
    }
    
    // Define el destructor de TarjetaDeCredito
    deinit { print("Tarjeta #(numero) se está eliminando de la memoria") }
}

// Crea una instancia de Cliente y la asigna a la variable juan
var juan: Cliente?
juan = Cliente(nombre: "Juan Pérez")

// Crea una instancia de TarjetaDeCredito y la asigna a la propiedad tarjeta de la instancia de Cliente creada anteriormente
juan!.tarjeta = TarjetaDeCredito(numero: 1234_5678_9012_3456, cliente: juan!)

juan = nil
// Imprime "Juan Pérez se está eliminando de la memoria"
// Imprime "Tarjeta #1234567890123456 se está eliminando de la memoria"

Conclusión

El Recuento Automático de Referencias (ARC) es una herramienta clave en Swift para gestionar la memoria de las instancias de clase. ARC asigna un bloque de memoria para almacenar información sobre una instancia y libera la memoria utilizada por esa instancia cuando ya no es necesaria. Para evitar que una instancia sea liberada antes de tiempo, ARC rastrea cuántas referencias fuertes están haciendo referencia a ella y no liberará la instancia mientras al menos una referencia activa a esa instancia todavía exista.

Aunque ARC “simplemente funciona” en la mayoría de los casos, es importante tener en cuenta las situaciones en las que se necesitan referencias débiles (weak) o referencias sin propiedad (unowned) y evitar la creación accidental de ciclos de referencia fuertes, que pueden provocar una fuga de memoria. En definitiva, entender cómo funciona ARC es fundamental para escribir código Swift eficiente y evitar errores de memoria.