Si deinit no se llama al cerrar una pantalla, hay un sospechoso que debe encabezar la lista: NSTimer (Timer en Swift).
Empecemos por la conclusión.
Un temporizador repetitivo retiene fuertemente su target.
El controlador de vista no se libera hasta llamar a
invalidate().
El último, el punto 52, de Effective Objective-C 2.0 trata por completo esta trampa clásica, que sigue siendo igual de válida en la era de Swift.
Resumen clave (3 puntos)
Timer.scheduledTimer(target:)mantiene el target mediante una referencia fuerte.- Si el controlador de vista guarda el temporizador como propiedad, se completa la referencia cíclica.
- Hay tres soluciones: API basada en bloques + weak, llamar a invalidate fuera de deinit y el patrón weak proxy.
Cómo se forma la referencia cíclica
El código problemático tiene este aspecto.
class PollingViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(
timeInterval: 5.0,
target: self, // El temporizador self lo retiene fuertemente
selector: #selector(refresh),
userInfo: nil,
repeats: true
)
}
deinit {
timer?.invalidate() // Nunca se llama
}
}
Si seguimos las referencias, queda así:
- Controlador de vista → retiene fuertemente el temporizador mediante la propiedad timer
- Temporizador → retiene fuertemente self, el controlador de vista que actúa como target
- Además, RunLoop → retiene fuertemente el temporizador
«¿No se puede llamar a invalidate en deinit?». Ese es el núcleo de la trampa. Como el temporizador retiene el controlador de vista, su contador de referencias nunca llega a 0 y deinit nunca se ejecuta. Aunque se haga pop de la pantalla, refresh sigue ejecutándose cada 5 segundos en segundo plano, provocando una fuga de memoria y un mayor consumo de batería.
Solución 1: API basada en bloques (iOS 10+)
Es la solución moderna más limpia. En lugar del enfoque basado en target, recibe un cierre y usa weak para las capturas.
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
self?.refresh()
}
RunLoop sigue reteniendo el temporizador, pero ya no existe un ciclo que retenga fuertemente el controlador de vista. Cuando este se libera, deinit puede llamar a invalidate correctamente.
La solución basada en categorías que propone Effective Objective-C tiene la misma esencia. Antes de iOS 10 no existía una API basada en bloques, así que recomendaba crear una categoría que pasara un bloque a NSTimer mediante userInfo.
Solución 2: llamar a invalidate según el ciclo de vida
Consiste en limpiar en el momento en que desaparece la pantalla, no en deinit.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
timer = nil
}
Al llamar a invalidate, el temporizador suelta la referencia fuerte al target y rompe el ciclo. Sin embargo, hay que recrearlo en viewWillAppear y considerar también los casos en que otra pantalla cubre brevemente la actual y luego desaparece, lo que aumenta los puntos de mantenimiento.
Solución 3: patrón weak proxy
Es un patrón clásico para situaciones que exigen pasar un target, como la compatibilidad con iOS 9 o CADisplayLink. Inserta entre el temporizador y el controlador de vista un delegado con solo una referencia débil.
final class WeakProxy: NSObject {
weak var target: NSObjectProtocol?
init(target: NSObjectProtocol) {
self.target = target
super.init()
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
target
}
}
timer = Timer.scheduledTimer(
timeInterval: 5.0,
target: WeakProxy(target: self), // El temporizador retiene fuertemente solo el proxy
selector: #selector(refresh),
userInfo: nil,
repeats: true
)
El temporizador solo retiene fuertemente el proxy, y este observa el controlador de vista únicamente mediante weak. Cuando el controlador se libera, los mensajes dirigidos al proxy pasan a nil mediante forwardingTarget y desaparecen silenciosamente. Es un caso de uso del reenvío de mensajes del runtime de Objective-C para resolver referencias cíclicas.
Como CADisplayLink todavía solo ofrece el enfoque basado en target, este patrón sigue vigente.
Resumen
- Temporizador repetitivo + target: self + guardado como propiedad = el trío de la referencia cíclica
- El plan de llamar a invalidate en deinit no es viable estructuralmente
- Como base, usa API basada en bloques +
[weak self]; para APIs que obligan a usar target, como CADisplayLink, usa weak proxy - Acostúmbrate a cerrar la pantalla y comprobar si aparece el registro de deinit: es la forma más barata de detectar este problema
Que el último punto de un libro de hace más de 10 años siga apareciendo en revisiones de código actuales demuestra que los frameworks cambian, pero los principios de las relaciones entre referencias permanecen.

