Swift y Objective-C

[Swift #1 intermedio] ARC: elige weak o unowned por su vida útil

En el capítulo sobre closures dejamos pendiente una pregunta al explicar cómo romper ciclos de referencias con [weak self]. También existe unowned: ¿qué diferencia hay y cuándo se usa? Para responder, primero hay que ver cómo funciona ARC internamente.

7 min de lectura
Imagen de portada de [Swift #1 intermedio] ARC: elige weak o unowned por su vida útil

En el capítulo sobre closures [weak self] dejamos pendiente una pregunta al explicar cómo romper ciclos de referencias. También existe unowned: ¿qué diferencia hay y cuándo se usa? Para responder, primero hay que ver cómo funciona ARC internamente.

Este es el primer artículo de la serie intermedia de Swift. Repasamos cómo funciona ARC, las diferencias exactas entre las referencias strong, weak y unowned, y los criterios prácticos para elegir entre ellas. La transición de MRC (Manual Reference Counting, conteo manual de referencias) de Objective-C a ARC se trata en otro artículo; aquí nos centramos en el presente desde la perspectiva de Swift.

La identidad de ARC: no es un recolector en tiempo de ejecución, sino un registro que ordena el compilador

Muchos conocen ARC (Automatic Reference Counting) como el «recolector de basura de Swift», pero su funcionamiento es fundamentalmente distinto.

La recolección de basura (GC) ejecuta periódicamente un sistema independiente en tiempo de ejecución que busca y limpia los objetos que «nadie usa». ARC, en cambio, se decide durante la compilación. El compilador analiza el flujo del código e inserta llamadas a retain (contador +1) donde aparece una referencia y a release (contador -1) donde termina. Cuando el contador llega a 0, se llama inmediatamente a deinit y se libera la memoria.

De esta diferencia se derivan dos propiedades importantes. Primero, la liberación es determinista. No es «ya se limpiará algún día», sino que ocurre justo en la línea donde desaparece la última referencia. Esa es la base para confiar la liberación de recursos a deinit. Segundo, no existe una fase de limpieza que detenga el programa. Es un ejemplo real de aquello de «no sacrificar rendimiento por seguridad» que vimos en la primera parte de la serie.

No es gratis. Los incrementos y decrementos del contador de referencias deben ser seguros entre hilos, por lo que se realizan como operaciones atómicas; ese es el coste oculto de los tipos por referencia. Esta es una de las razones por las que Swift favorece struct por defecto: los tipos por valor no necesitan este registro. Y, crucialmente, ARC no puede romper ciclos. El GC encuentra grupos de objetos inalcanzables desde las raíces y los limpia aunque formen ciclos, pero en ARC el contador no puede llegar a 0 mientras se sigan contando mutuamente. Por eso los ciclos de referencias siguen siendo responsabilidad del programador.

Tres referencias: el lenguaje de la propiedad

En el mundo de ARC, las referencias se dividen en tres tipos según si somos responsables de mantener vivo el objeto.

strong (valor predeterminado) significa propiedad. Incrementa el contador y el objeto permanece vivo mientras lo conservemos. Toda referencia declarada sin una marca explícita es strong.

weak es una observación sin propiedad. Como no incrementa el contador, el objeto se libera cuando desaparecen las demás referencias strong. En ese momento, el runtime convierte automáticamente la referencia weak en nil. Por eso una variable weak debe ser opcional y var. El hecho de que «pueda convertirse en nil en cualquier momento» queda reflejado en su tipo, como vimos en el capítulo sobre opcionales.

unowned también es una referencia sin propiedad, pero renuncia al tratamiento de nil. Al igual que weak, no incrementa el contador, pero acceder a ella después de liberar el objeto provoca un fallo inmediato en lugar de nil. A cambio, no es opcional y puede usarse sin desenvolverla.

En resumen: strong dice «te mantendré con vida»; weak, «sé que puedes desaparecer»; unowned, «estoy seguro de que vivirás más que yo».

strong es propiedad; weak observa sabiendo que puede ser nil; unowned mira directamente con esa certeza
strong es propiedad; weak observa sabiendo que puede ser nil; unowned mira directamente con esa certeza

weak frente a unowned: el criterio es la relación de vida útil

Entonces, ¿cuándo usar weak y cuándo unowned? El criterio no es la sintaxis, sino la relación de vida útil entre los dos objetos.

Si el otro puede desaparecer antes que yo, usa weak. El delegate es el caso clásico. Cuando una vista referencia a su delegate (normalmente un view controller), es perfectamente normal que este se libere primero. Por eso la propiedad delegate suele declararse weak var delegate y, al usarla, el optional chaining (delegate?.didFinish()) expresa «ignorar si no existe». Es una relación en la que nil es un estado normal.

Si está garantizado estructuralmente que el otro vive tanto como yo o más, usa unowned. El caso clásico es una tarjeta de crédito y su cliente. La tarjeta no puede existir sin el cliente, y este debe seguir vivo mientras la tarjeta exista. unowned let customer es la expresión exacta. Como no es opcional, no hay ruido de desenvoltura en cada uso y puede declararse con let, preservando la inmutabilidad.

Al superponer este criterio con el de la desenvoltura forzada ! aparece una coherencia. unowned es la ! versión de una referencia. Declara «si aquí hay nil, mi diseño se rompió» y, si la certeza resulta falsa, informa inmediatamente mediante un fallo en lugar de un comportamiento silenciosamente incorrecto. Si no estás seguro, usa weak. En la práctica, la regla general es «si hay dudas, weak»: su coste (tratar un opcional y una pequeña sobrecarga de runtime) es menor que el riesgo de un fallo. Piensa en unowned como una herramienta que debe reservarse para los casos donde la garantía de vida útil se lee claramente en la estructura del código.

El mismo criterio se aplica a la captura de closures. [weak self] es la opción predeterminada porque, en la mayoría de los casos, no hay garantía de que self siga vivo cuando se ejecute el closure. En cambio, unowned está justificado cuando las vidas del closure y self están vinculadas; por ejemplo, al capturar self en un closure de ejecución inmediata de una propiedad lazy. Si la propiedad está viva, self también lo está.

Comprobar con herramientas: instrumentación, no intuición

Los ciclos de referencias no siempre se detectan mediante revisión de código. Conocer tres herramientas convierte el «probablemente esté bien» en «lo he comprobado».

Los logs de deinit son la herramienta más barata. Si cierras la pantalla y no aparece el deinit del view model, hay una fuga en alguna parte. Añadir un print a las clases sospechosas durante el desarrollo permite detectar pronto la mayoría de los ciclos.

El Memory Graph Debugger de Xcode muestra los ciclos visualmente. Al pulsar el botón del gráfico de memoria en la barra de depuración durante la ejecución, aparecen los objetos del heap actual y sus relaciones de referencia. Si quedan objetos que deberían haberse liberado, puedes seguir con flechas quién los retiene. Los objetos sospechosos de fuga aparecen con un signo de exclamación morado, otra pista útil.

La plantilla Leaks de Instruments realiza instrumentación a lo largo del tiempo. Es adecuada para las revisiones periódicas antes de publicar: abre y cierra repetidamente la pantalla y observa si la memoria crece en forma de escalera.

Por cierto, los sospechosos habituales de los ciclos están bien definidos: closures almacenados en propiedades (capítulo sobre closures), declarar por error el delegate como strong y no cancelar registros en NotificationCenter o temporizadores (capítulo sobre NSTimer). Revisar estos tres puntos al revisar código nuevo cubre la mayoría de los ciclos.

Instrumentación, no intuición: logs de deinit, gráfico de memoria y Leaks
Instrumentación, no intuición: logs de deinit, gráfico de memoria y Leaks

Resumen

  • ARC no es un recolector en tiempo de ejecución: inserta retain/release durante la compilación. La liberación es determinista y no hay pausas del GC, pero no puede romper ciclos por sí solo.
  • Las referencias son el lenguaje de la propiedad. strong posee; weak no posee y admite nil; unowned no posee y afirma que el otro vivirá más tiempo.
  • El criterio es la relación de vida útil: weak si el otro puede desaparecer primero; unowned si estructuralmente vivirá más; weak si hay dudas.
  • unowned es la desenvoltura forzada de las referencias. Úsalo solo donde la certeza sea evidente en el código.
  • Compruébalo mediante instrumentación, no intuición, con logs de deinit, Memory Graph Debugger e Instruments Leaks.

El próximo artículo es el segundo de la serie intermedia: genéricos. Veremos cómo la T entre corchetes angulares consigue seguridad de tipos y reutilización de código, y cuándo hace falta una cláusula where.

Artículos recomendados