Saltar al contenido
K
Swift: memoria y concurrenciaPaso a paso35 min de lectura · 5 min de máquina Intermedio

ARC

Al terminar: Entender cómo ARC cuenta referencias, y qué son strong, weak y unowned.

Al terminar tendrás en el paquete Torneo una clase PartidoEnVivo que se está jugando, un Narrador que lo cuenta sin mantenerlo vivo y una hoja de estadísticas, Estadillo, que no existe sin su partido. torneo-cli imprimirá PartidoEnVivo liberado (deinit) justo al cerrar un bloque. También sabrás por qué dos objetos que se apuntan entre sí pueden quedarse en memoria para siempre.

Importa porque en Swift la memoria de los objetos la gobierna una regla que tú puedes ver y romper: un objeto vive mientras alguien lo sujete con fuerza. Kotlin te lo esconde. Swift te obliga a decidir quién sujeta a quién.

Temas: ARC, strong, weak, unowned y deinit. La lección siguiente, unowned y tests de memoria, ve qué pasa cuando un unowned falla y comprueba la liberación con tests.

ARC: cada objeto lleva la cuenta de quién lo sujeta

Una referencia es una variable que apunta a un objeto. ARC (Automatic Reference Counting, «recuento automático de referencias») añade a cada instancia de una clase un contador con las referencias fuertes que le apuntan. Cuando el contador llega a cero, Swift libera el objeto en ese mismo instante y ejecuta su deinit.

Hay tres formas de apuntar a un objeto:

TipoSe escribe¿Suma al contador?¿Es opcional?Si el objeto ya no existe
Fuerte (strong)var narrador: Narrador? (es la forma normal)SíComo quierasNo puede pasar: el objeto sigue vivo
Débil (weak)weak var partido: PartidoEnVivo?NoSí, siempre, y siempre varVale nil automáticamente
Sin dueño (unowned)unowned let partido: PartidoEnVivoNoNoEl programa se detiene

Una clase es un tipo que se pasa por referencia. Un struct o un enum no tienen contador porque se copian. Por eso todo lo de esta lección habla de clases.

Ejemplo mínimo: contar referencias

deinit es un bloque de una clase que Swift ejecuta una sola vez, justo cuando el contador llega a cero. No se llama a mano. Pruébalo en tu Playground, el de Tu primer paquete de Swift: reutilízalo y borra lo anterior antes de pegar cada ejemplo.

ejemplo mínimo: contar referencias (con comentarios)
final class Balon {
    let nombre: String
    init(_ nombre: String) { self.nombre = nombre }
    deinit { print("\(nombre) liberado") }   // corre una vez, cuando el contador llega a 0
}
 
var a: Balon? = Balon("Nike")   // contador: 1
var b = a                       // contador: 2
a = nil                         // contador: 1, no pasa nada
print("a vale nil, pero b lo mantiene")
b = nil                         // contador: 0 → deinit
print("b vale nil")

Lo verás en la consola así, en este orden:

a vale nil, pero b lo mantiene
Nike liberado
b vale nil

Nike liberado sale entre las dos líneas, en el instante en que b suelta el balón. No sale «cuando Swift pueda».

Kotlin y Swift: recolector de basura frente a ARC

QuéKotlin en la JVM (Android)Swift
Quién liberaEl recolector de basura (garbage collector): busca los objetos a los que ya no se llega desde el programaARC: cada objeto cuenta sus referencias fuertes
CuándoCuando el recolector pasa; no hay un momento fijoEn el instante en que el contador llega a cero
Ciclo A → B → ASe libera si nadie de fuera llega a ellosFuga: ninguno llega a cero
Código al liberarNada que puedas usar para estodeinit, en un momento fijo y conocido
Fuga típicaAlgo alcanzable retiene un Context o una ActivityUn ciclo de referencias fuertes

Por eso weak y unowned no tienen equivalente diario en Kotlin: existen porque Swift cuenta referencias, y rompen los ciclos que ese recuento no puede resolver.

Las tres referencias juntas

Antes del ejemplo, dos piezas nuevas:

  • ?() al llamar una closure opcional. Una propiedad de tipo (() -> Void)? guarda «una closure sin parámetros que no devuelve nada», o nil (lo de () -> Void ya lo viste en Closures). Para llamarla se escribe alTerminar?(): el ? hace que solo se ejecute si no es nil.
  • do { … } sin catch. Es un bloque suelto: lo que creas dentro (constantes, objetos) deja de existir al cerrar la llave. Sirve para decidir justo dónde termina la vida de una variable.

El ejemplo junta las tres referencias: un concierto, su presentador y una entrada. El guard let concierto else { … } es el que ya conoces de guard let.

ejemplo mínimo: fuerte, weak, unowned y una closure en deinit (con comentarios)
final class Concierto {
    let artista: String
    // Una closure guardada y opcional: () -> Void es «sin parámetros, no devuelve nada».
    private let alTerminar: (() -> Void)?
    var presentador: Presentador?              // fuerte: el concierto mantiene vivo al presentador
 
    init(artista: String, alTerminar: (() -> Void)? = nil) {
        self.artista = artista
        self.alTerminar = alTerminar
    }
 
    deinit {
        alTerminar?()                          // ?(): la llama solo si no es nil
    }
}
 
final class Presentador {
    let nombre: String
    weak var concierto: Concierto?             // weak: no suma al contador; siempre var y opcional
 
    init(nombre: String) { self.nombre = nombre }
 
    func anuncio() -> String {
        guard let concierto else { return "\(nombre): no hay concierto" }   // nil si ya se liberó
        return "\(nombre): con ustedes, \(concierto.artista)"
    }
}
 
final class Entrada {
    private unowned let concierto: Concierto   // unowned: no suma y no es opcional
    init(concierto: Concierto) { self.concierto = concierto }
    var artista: String { concierto.artista }  // si el concierto ya no existiera, se detendría
}
 
let presentador = Presentador(nombre: "Rosa")
// do { … } sin catch: un bloque. Lo creado dentro deja de existir al cerrar la llave.
do {
    let concierto = Concierto(artista: "Los Grillos", alTerminar: { print("Concierto liberado") })
    concierto.presentador = presentador        // concierto → presentador: fuerte
    presentador.concierto = concierto          // presentador → concierto: weak, no hay ciclo
    print(Entrada(concierto: concierto).artista)
    print(presentador.anuncio())
}                                              // aquí el contador del concierto llega a 0
print(presentador.anuncio())                   // weak ya vale nil

Se ve así:

Los Grillos
Rosa: con ustedes, Los Grillos
Concierto liberado
Rosa: no hay concierto

El concierto se libera al cerrar el do, porque lo único que lo apuntaba desde fuera era el weak del presentador.

Error típico: dos referencias fuertes que se apuntan

Tu PartidoEnVivo conocerá a su narrador y el Narrador conocerá a su partido. Si las dos referencias fueran fuertes, cada uno mantendría vivo al otro. Estas dos clases con el bug (PartidoConCiclo y NarradorConCiclo) no las escribirás en tu paquete: en la lección siguiente viven dentro del fichero de tests, para no ensuciarlo.

con el bug: las dos referencias son fuertes
private final class PartidoConCiclo {
    var narrador: NarradorConCiclo?
}
 
private final class NarradorConCiclo {
    var partido: PartidoConCiclo?   // fuerte: debería ser `weak`
}

Y así queda el arreglo, en una clase mínima: una sola palabra, weak, y el ciclo desaparece. (La versión de verdad de Narrador la escribes en el paso.)

corregido: el narrador mira al partido con weak
private final class NarradorSinCiclo {
    weak var partido: PartidoConCiclo?   // weak: no suma al contador
}

Para verlo, se crean los dos objetos dentro de un bloque do { }:

cómo se forma el ciclo
do {
    let partido = PartidoConCiclo()
    let narrador = NarradorConCiclo()
    partido.narrador = narrador
    narrador.partido = partido
}   // las variables locales desaparecen, pero cada objeto sigue sujeto por el otro

Falla porque, cuando sales del bloque, las variables locales desaparecen pero cada objeto sigue con contador 1 (lo sujeta el otro): ninguno llega a cero, deinit no corre y la memoria no se recupera. No hay error ni aviso: el programa sigue funcionando. Se corrige preguntando quién es el dueño: el partido es dueño de su narrador, y el narrador solo lo mira, así que el narrador usa weak.

Reto · Ejercicio: el equipo y su utillero

En tu Playground (reutiliza el mismo y borra lo anterior), escribe dos clases, Equipo y Utillero (la persona que guarda el material del equipo), con un nombre cada una, que se conozcan entre sí: el equipo sabe quién es su utillero y el utillero sabe de qué equipo es. Cuando se libera, cada una imprime un aviso con su nombre, como «Equipo Toros liberado». Escribe una función, prueba (dentro de una función porque las variables globales, las que están fuera de cualquier función, viven hasta el final del programa y no se liberan antes), que cree el equipo Toros y el utillero Rosa, los conecte en los dos sentidos e imprima «fin de prueba»; llámala y después imprime «después». Con las dos referencias fuertes, lo que debe salir es:

fin de prueba
después

Ahora haz weak la referencia del utillero hacia su equipo, para que los dos se liberen. Lo que debe salir entonces:

fin de prueba
Equipo Toros liberado
Utillero Rosa liberado
después

Piensa por qué conviene weak en esa y no en la otra. Pista: un equipo sigue existiendo aunque cambie de utillero; un utillero sin equipo no tiene nada que guardar.

Ver solución
solución: el equipo y su utillero
final class Equipo {
    let nombre: String
    var utillero: Utillero?              // el equipo es dueño de su utillero
    init(_ nombre: String) { self.nombre = nombre }
    deinit { print("Equipo \(nombre) liberado") }
}
 
final class Utillero {
    let nombre: String
    weak var equipo: Equipo?             // el utillero solo mira: weak rompe el ciclo
    init(_ nombre: String) { self.nombre = nombre }
    deinit { print("Utillero \(nombre) liberado") }
}
 
func prueba() {
    let e = Equipo("Toros")
    let u = Utillero("Rosa")
    e.utillero = u
    u.equipo = e
    print("fin de prueba")
}
 
prueba()
print("después")

Con las dos referencias fuertes, no sale ningún deinit. Con weak en Utillero.equipo, la consola muestra:

fin de prueba
Equipo Toros liberado
Utillero Rosa liberado
después

Cualquiera de las dos weak rompería el ciclo, pero conviene ponerla en la que depende de la otra: aquí el utillero existe para el equipo y no al revés. Si el equipo guardara al utillero con weak, el utillero se liberaría en cuanto nadie más lo sujetara, aunque el equipo lo siga necesitando (y las dos líneas de «liberado» podrían salir en otro orden).

Paso · PartidoEnVivo, el narrador y el estadillo

Objetivo: una clase PartidoEnVivo con un narrador, una hoja de estadísticas (Estadillo) y un bloque en torneo-cli que demuestra que el partido se libera al salir de su bloque.

Dónde trabajas. En el paquete Torneo, abierto en Xcode. Los comandos xcrun swift … de esta lección se ejecutan todos desde ~/Marcador-Swift/Torneo, la carpeta donde está Package.swift.

Torneo
Sources
Torneo
Ejemplo.swiftse crea y se rellena con «Copiar fichero»
PartidoEnVivo.swiftse crea y se escribe
torneo-cli
main.swiftse modifica
Un fichero nuevo que se copia, uno que se escribe y uno que cambia. Así los ves en el Project navigator.

Paso 1 · Los datos de ejemplo: Ejemplo.swift

Ejemplo.swift son datos de ejemplo (jugadores, equipos y dos guiones de partido) que la consola y los tests usan tal cual. No enseña nada de memoria, así que no lo escribes: lo copias. Usa dos piezas que todavía no has escrito:

static y el enum sin casos. Una propiedad static pertenece al tipo, no a una instancia: se lee como Ejemplo.ana, sin crear ningún Ejemplo. Un enum sin casos no se puede instanciar, así que es la forma habitual de agrupar constantes y funciones bajo un nombre (un espacio de nombres). El equivalente en Kotlin es object Ejemplo.

try!, que ya escribiste en Errores: throws, try y do/catch: promete que no va a fallar y, si falla, el programa se detiene. Aquí vale porque son datos escritos a mano: si estuvieran mal, se vería al primer arranque. Y Equipo(…)!: el init de Equipo puede fallar y por eso devuelve un opcional (Equipo?); el ! lo abre a la fuerza, como en Struct y class. Aquí los datos son válidos, así que nunca será nil.

  1. Crea el fichero. En el Project navigator, clic derecho sobre la carpeta Torneo que está dentro de Sources (no sobre la de Tests) → New File from Template… → Swift File → Next, escribe el nombre Ejemplo y pulsa Create. En un paquete Swift el fichero pertenece al objetivo Torneo por estar en su carpeta: no hay que marcar ningún objetivo. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal con touch Sources/Torneo/Ejemplo.swift y Xcode lo muestra solo.
  2. Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
  3. Haz clic dentro del editor con el fichero abierto, selecciona todo con Cmd+A y pega con Cmd+V. Lo pegado reemplaza la plantilla que Xcode pone en un fichero nuevo.
Ejemplo.swift

Puedes ojearlo: son static let con jugadores, equipos y dos partidos de ejemplo, Ejemplo.rayoContraToros y Ejemplo.lobosContraAguilas.

Paso 2 · La clase PartidoEnVivo, Narrador y Estadillo

Crea PartidoEnVivo.swift igual que antes: clic derecho sobre la carpeta Torneo de Sources → New File from Template… → Swift File → Next, escribe PartidoEnVivo y pulsa Create.

Aquí no hay piezas nuevas: todo está en el ejemplo del concierto y en lo que ya escribiste. Solo recuerda tres cosas:

  • public final class: public, como en todas tus clases de Torneo, la hace visible desde torneo-cli; final, que nadie hereda de ella.
  • weak var partido: PartidoEnVivo? y unowned let partido: PartidoEnVivo son las dos referencias sin dueño de la tabla de arriba, y alLiberarse: (() -> Void)? es la closure que se llama desde deinit, como la de Concierto. Sirve para que la consola vea el deinit.
  • Nombres exactos que usan main.swift y los tests de la lección siguiente: PartidoEnVivo se crea con las etiquetas guion y alLiberarse (esta, opcional) y tiene la propiedad narrador; Narrador se crea con nombre y tiene partido y frase; Estadillo se crea con partido y tiene golesLocal. La frase del narrador tiene la forma «Pepe: Rayo FC contra Toros», con los nombres de los equipos del guion.

Intenta escribirlo tú, en PartidoEnVivo.swift, tres clases públicas de las que nadie hereda:

  1. PartidoEnVivo, con su guion (un Partido), un narrador opcional al que mantiene vivo, y una closure opcional que recibe al crearse y que llama justo cuando se libera.
  2. Narrador, con su nombre y el partido que narra, mirado sin mantenerlo vivo; su frase dice «Pepe: Rayo FC contra Toros» o, si el partido ya no existe, «Pepe: ya no hay partido».
  3. Estadillo, que guarda su partido sin mantenerlo vivo ni tratarlo como opcional (un estadillo no existe sin su partido), y da los goles del local.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/Torneo/PartidoEnVivo.swift
// Un partido que se está jugando. `guion` es todo lo que va a pasar.
//
// Es una CLASE, y no un struct como `Partido`, porque tiene IDENTIDAD y vida:
// alguien lo empieza, varios lo miran y, cuando ya nadie lo necesita, se libera.
// ARC (Automatic Reference Counting) cuenta cuántas referencias FUERTES apuntan
// a cada objeto y lo libera cuando llegan a cero. Los structs y enums no lo
// necesitan: se copian.
public final class PartidoEnVivo {
    public let guion: Partido
 
    // Se llama en `deinit`, es decir, justo cuando ARC libera el objeto. Sirve
    // para que las pruebas (y la consola) vean que de verdad se liberó.
    private let alLiberarse: (() -> Void)?
 
    // `strong` es lo normal: esta referencia mantiene vivo al narrador.
    public var narrador: Narrador?
 
    public init(guion: Partido, alLiberarse: (() -> Void)? = nil) {
        self.guion = guion
        self.alLiberarse = alLiberarse
    }
 
    // `deinit` corre UNA vez, cuando el contador de referencias fuertes llega a
    // cero. No se llama a mano. (Kotlin no tiene nada igual: el recolector de
    // basura libera «cuando puede», y no hay un momento fijo.)
    deinit {
        alLiberarse?()
    }
}
 
// El narrador cuenta el partido. Necesita saber de qué partido habla, y el
// partido sabe quién lo narra: dos objetos que se apuntan entre sí.
//
// Si las DOS referencias fueran fuertes, ninguno de los dos llegaría nunca a
// contador cero (cada uno mantiene vivo al otro) y no se liberarían jamás:
// un ciclo de retención (retain cycle). La solución es que UNA de las dos sea
// `weak`: no suma al contador y se pone a `nil` cuando el objeto se libera.
// La pregunta es «¿quién es el dueño?». El partido es el dueño del narrador,
// no al revés, y por eso el narrador lo mira con `weak`.
public final class Narrador {
    public let nombre: String
    public weak var partido: PartidoEnVivo?
 
    public init(nombre: String) {
        self.nombre = nombre
    }
 
    public func frase() -> String {
        // Como `partido` es `weak`, es opcional: hay que abrirlo.
        guard let partido else { return "\(nombre): ya no hay partido" }
        return "\(nombre): \(partido.guion.local.nombre) contra \(partido.guion.visitante.nombre)"
    }
}
 
// `unowned`: como `weak` pero NO opcional. Se usa cuando el otro objeto siempre
// vive al menos tanto como éste: un estadillo no existe sin su partido. Si te
// equivocas y el partido ya no está, el programa se detiene (a diferencia de
// `weak`, que te da `nil`). Es más cómodo y más peligroso.
public final class Estadillo {
    private unowned let partido: PartidoEnVivo
 
    public init(partido: PartidoEnVivo) {
        self.partido = partido
    }
 
    public var golesLocal: Int { partido.guion.golesLocal }
}

Paso 3 · Soltar el partido en main.swift

Abre main.swift (en Sources, dentro de torneo-cli). Al final del fichero, después del último } de lo que ya tienes, intenta escribir un bloque como el del ejemplo del concierto, cuyas constantes desaparecen al cerrar la llave:

  1. Dentro del bloque, crea el partido en vivo de Rayo contra Toros, que al liberarse imprime «PartidoEnVivo liberado (deinit)».
  2. Crea a Pepe como narrador y enlázalos en los dos sentidos.
  3. Imprime la frase del narrador y avisa de que al salir se libera el partido.
  4. Después del bloque, escribe una última línea.

Lo que debes ver al ejecutarlo, al final de la salida:

Pepe: Rayo FC contra Toros
Al salir del bloque, ARC libera el partido...
PartidoEnVivo liberado (deinit)
...y ya no hay nada que narrar.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/torneo-cli/main.swift
// ARC: el partido se libera cuando nadie lo mira.
do {
    let enVivo = PartidoEnVivo(guion: Ejemplo.rayoContraToros, alLiberarse: { print("PartidoEnVivo liberado (deinit)") })
    let narrador = Narrador(nombre: "Pepe")
    enVivo.narrador = narrador
    narrador.partido = enVivo
    print(narrador.frase())
    print("Al salir del bloque, ARC libera el partido...")
}
print("...y ya no hay nada que narrar.")

Ejecútalo:

xcrun swift run torneo-cli

La primera vez compila (tarda unos segundos). Las cuatro últimas líneas son las del bloque:

Pepe: Rayo FC contra Toros
Al salir del bloque, ARC libera el partido...
PartidoEnVivo liberado (deinit)
...y ya no hay nada que narrar.

Si prefieres Xcode: arriba, en el centro de la barra de herramientas, el selector de esquema (scheme) debe decir torneo-cli y el destino (destination), My Mac; Product → Run (Cmd+R) lo ejecuta y la salida aparece en la consola, el panel de abajo (el Debug area; se muestra con Cmd+Shift+Y).

Paso 4 · Rompe el weak

Una prueba corta que enseña más que leerla. En PartidoEnVivo.swift, quita la palabra weak de public weak var partido: PartidoEnVivo? (en Narrador). Ahora las dos referencias son fuertes: es el ciclo del error típico. Ejecuta xcrun swift run torneo-cli: la línea PartidoEnVivo liberado (deinit) ya no sale, y no hay ningún error ni aviso. Vuelve a escribir weak y comprueba que la línea reaparece.

Punto de control

  • xcrun swift run torneo-cli termina con estas cuatro líneas, en este orden: Pepe: Rayo FC contra Toros, Al salir del bloque, ARC libera el partido..., PartidoEnVivo liberado (deinit) y ...y ya no hay nada que narrar.
  • xcrun swift test sigue en Test run with 89 tests in 11 suites passed, y xcrun swift build --build-tests no muestra ningún aviso.
  • Quitaste el weak, viste desaparecer el deinit sin ningún error y lo volviste a poner.
  • Sabes decir por qué Narrador.partido es weak y no fuerte.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Cuándo se ejecuta el deinit de una instancia de una clase?

  2. 2.¿Cómo tiene que declararse una referencia weak?

  3. 3.Un PartidoConCiclo guarda una referencia fuerte a su NarradorConCiclo, y el narrador guarda una referencia fuerte al partido. Sales del bloque donde creaste los dos. ¿Qué pasa?

  4. 4.Dos objetos de Kotlin en la JVM se apuntan entre sí y nadie más llega a ellos. ¿Por qué no hace falta un weak como en Swift?

  5. 5.En el ejemplo del balón, ¿cuándo sale Nike liberado?

Faltan 5 de 5