Se deinit não for chamado depois de fechar uma tela, coloque um suspeito no topo da lista: NSTimer (Timer no Swift).
Vamos começar pela conclusão.
Um timer repetitivo retém o target fortemente.
O view controller nunca é liberado até
invalidate()ser chamado.
O último item, o de número 52, de Effective Objective-C 2.0 trata completamente dessa armadilha clássica, que continua igualmente válida na era do Swift.
Resumo principal (3 pontos)
Timer.scheduledTimer(target:)mantém o target com uma referência forte.- Quando o view controller mantém o timer em uma propriedade, o ciclo de retenção é concluído.
- Há três soluções — API baseada em blocos + weak, chamar invalidate fora de deinit e o padrão weak proxy.
Como o ciclo de retenção é formado
O código problemático é assim.
class PollingViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(
timeInterval: 5.0,
target: self, // O timer self o retém fortemente
selector: #selector(refresh),
userInfo: nil,
repeats: true
)
}
deinit {
timer?.invalidate() // Nunca é chamado
}
}
Seguindo as referências, temos isto:
- View controller → retém o timer fortemente pela propriedade timer
- Timer → retém fortemente self, o view controller que é o target
- Além disso, RunLoop → retém o timer fortemente
“Não dá para chamar invalidate no deinit?” Esse é o ponto central da armadilha. Como o timer retém o view controller, a contagem de referências nunca chega a 0 e deinit nunca é executado. Mesmo depois de dar pop na tela, o refresh continua rodando a cada 5 segundos em segundo plano, causando vazamento de memória e de bateria.
Solução 1: API baseada em blocos (iOS 10+)
É a solução moderna mais limpa. Em vez do modo baseado em target, receba uma closure e faça as capturas usando weak.
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
self?.refresh()
}
O timer continua retido pelo RunLoop, mas não há um ciclo que retenha o view controller fortemente. Quando ele é liberado, deinit chama invalidate normalmente.
A solução com category proposta por Effective Objective-C tem a mesma essência. Antes do iOS 10, não havia API baseada em blocos, então a recomendação era criar uma category que passasse um bloco ao NSTimer por meio de userInfo.
Solução 2: chamar invalidate conforme o ciclo de vida
A ideia é fazer a limpeza em o momento em que a tela desaparece, e não no deinit.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
timer = nil
}
Ao chamar invalidate, o timer solta a referência forte ao target e o ciclo é quebrado. Porém, é preciso recriá-lo no viewWillAppear e considerar também quando outra tela cobre a atual por instantes e depois sai, aumentando os pontos de manutenção.
Solução 3: padrão weak proxy
Este é um padrão clássico para situações que exigem passar um target, como suporte ao iOS 9 e CADisplayLink. Insira entre o timer e o view controller um delegado com apenas uma referência fraca.
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), // O timer retém fortemente apenas o proxy
selector: #selector(refresh),
userInfo: nil,
repeats: true
)
O timer retém fortemente apenas o proxy, e o proxy observa o view controller somente com weak. Quando o view controller é liberado, as mensagens enviadas ao proxy passam por forwardingTarget até nil e desaparecem silenciosamente. É um caso de aplicação do encaminhamento de mensagens do runtime do Objective-C para resolver ciclos de retenção.
Como o CADisplayLink ainda oferece apenas o modo baseado em target, esse padrão continua atual.
Resumo
- Timer repetitivo + target: self + armazenado em propriedade = o trio do ciclo de retenção
- O plano de chamar invalidate no deinit não é viável estruturalmente
- Por padrão, use API baseada em blocos +
[weak self]; em APIs que exigem target, como CADisplayLink, use weak proxy - Crie o hábito de fechar a tela e verificar se o log de deinit aparece; é a forma mais barata de detectar esse problema
Ver o último item de um livro com mais de 10 anos ainda ser apontado em code reviews atuais mostra que os frameworks mudam, mas os princípios das relações entre referências permanecem os mesmos.

