Retain Cycle é uma situação em ARC onde dois ou mais objetos se referenciam mutuamente por meio de referências fortes, formando um ciclo fechado. De acordo com o Apple Memory Management Guide, 2026, um retain cycle bloqueia a liberação de todos os objetos no ciclo porque cada um tem um retain count ≥ 1. Diferentemente de um vazamento de memória em GC, um retain cycle garante que os objetos permaneçam vivos enquanto pelo menos um participante externo do ciclo estiver vivo — e mesmo após perder todas as referências externas, se o ciclo estiver isolado.
Principais pontos
Retain Cycle é uma situação em que dois ou mais objetos possuem um ao outro por meio de referências fortes, criando um grafo de dependências fechado. ARC não pode liberar nenhum desses objetos porque o retain count de cada um é sempre ≥ 1: o objeto A retém B, B retém A, e seus contadores nunca chegam a zero.
O problema ocorre exclusivamente em sistemas de contagem de referências (ARC, MRR). No Garbage Collection, o coletor determina a inacessibilidade por meio do grafo de referências a partir do conjunto raiz — ciclos não são um obstáculo. No ARC, no entanto, um ciclo equivale a um vazamento, porque a liberação determinística por contagem não pode resolver dependências circulares.
De acordo com a WWDC 2012 Session 406, retain cycle é a causa mais comum de vazamentos de memória em aplicativos Objective-C e Swift. Cenários típicos: relacionamentos pai-filho com delegados, closures que capturam self e arquiteturas em camadas com relacionamentos bidirecionais.
Vamos examinar cenários clássicos de retain cycle que todo desenvolvedor iOS encontra. Compreender esses padrões é a base para escrever código seguro com ARC.
Cenário clássico: um objeto pai (por exemplo, UIViewController) cria um objeto filho e se torna seu delegado. Se ambos usarem referências fortes, ocorre um retain cycle. A solução — o delegado deve ser weak.
// ERRO: retain cycle por meio de delegate forte
protocol ChildDelegate: AnyObject { }
class ParentVC: UIViewController, ChildDelegate {
var child: ChildVC?
func showChild() {
child = ChildVC()
child?.delegate = self // Parent → Child (strong)
} // Child → Parent (strong via delegate)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong por padrão
}
// CORREÇÃO: delegate weak
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — não retém
}
No exemplo, ParentVC mantém uma referência forte a ChildVC por meio da propriedade child. ChildVC mantém uma referência forte a ParentVC por meio de delegate. O ciclo está fechado. Correção: weak var delegate — a referência não aumenta o retain count, e ParentVC pode ser liberado.
NSTimer é uma fonte clássica de retain cycles. O timer retém seu target (geralmente self), e o target retém o timer por meio de uma propriedade. Mesmo que o timer seja de uso único, ele não será liberado até que invalidate seja chamado. Solução: sempre chamar timer.invalidate() em deinit ou viewDidDisappear.
Em arquiteturas com propriedade em cascata (coordenadores, roteadores), ciclos de múltiplas etapas frequentemente ocorrem: Coordinator → ViewController → ViewModel → Coordinator (por meio de um callback). Cada referência forte na cadeia deve ser conscientemente escolhida — uma referência weak em qualquer elo quebra o ciclo.
Closures em Swift capturam variáveis externas por referência forte. Se uma closure for armazenada como propriedade de um objeto (por exemplo, um completion handler) e capturar self, ela cria um retain cycle: self → closure → self.
Esta é a fonte mais comum de retain cycles no desenvolvimento moderno com Swift. Ocorre implicitamente — um desenvolvedor pode não notar a captura de self em uma closure, especialmente ao usar sintaxe abreviada sem self explícito.
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ Retain cycle: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ Correção: capture list com weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
Uma lista de captura [weak self] cria uma referência fraca a self dentro da closure. Se DownloadService for liberado antes da execução da closure, self se torna nil, e o código sai com segurança por meio de guard. Este é um padrão padrão para closures assíncronas em Swift — deve ser usado sempre que uma closure for armazenada como propriedade.
unowned self é uma alternativa a weak self quando self tem garantia de viver mais que a closure. Exemplo: closures síncronas que executam imediatamente (sorted, filter). Nesses casos, self está definitivamente vivo, e unowned é seguro. No entanto, unowned trava ao acessar um objeto liberado — portanto, weak é considerado a opção segura padrão.
Detectar retain cycles em um estágio inicial é criticamente importante para o desempenho do aplicativo. Vamos revisar as principais ferramentas e técnicas para identificar referências cíclicas no desenvolvimento iOS.
Xcode Memory Debugger (Debug Memory Graph) é uma ferramenta visual que mostra o grafo de objetos na memória com suas referências. Um retain cycle aparece como uma cadeia fechada de setas fortes. Para iniciar: clique no botão Debug Memory Graph no painel Debug area enquanto o aplicativo está em execução. Cada objeto é mostrado com seu tipo, endereço e lista de referências.
Instruments Leaks é um profiler para detecção automática de vazamentos. Ele grava alocações e analisa o grafo de referências em tempo real. Detecta não apenas retain cycles, mas também referências esquecidas, ViewControllers não liberados e outros vazamentos. Leaks aponta o objeto exato e a cadeia de retenção.
O método mais simples é adicionar um print no deinit de cada classe chave. Se deinit não for chamado quando se espera que o objeto seja destruído, há um retain cycle. Este método não requer ferramentas e é eficaz para diagnóstico inicial.
| Ferramenta | Tipo | Quando usar |
|---|---|---|
| Memory Debugger | Grafo visual | Verificação manual após navegação |
| Instruments Leaks | Análise automatizada | Testes de regressão, CI |
| deinit print | Registro manual | Desenvolvimento, code review |
| Malloc Scribble | Sinalizador de runtime | Depuração de use-after-free |
Abordagem recomendada: use registro de deinit durante o desenvolvimento, Memory Debugger durante os testes manuais e Instruments Leaks no pipeline de CI/CD para detecção automatizada de regressões de vazamento.
Prevenir retain cycles é mais fácil do que corrigi-los em produção. Aqui estão algumas regras que minimizam o risco de referências cíclicas.
Todos os delegados e dataSources devem ser weak. Esta regra está incorporada no UIKit: todos os protocolos de delegados no SDK da Apple são declarados com propriedades weak (UITableView.delegate, UICollectionView.dataSource). Para seus próprios protocolos, use weak var delegate: MyDelegate? e herde o protocolo de AnyObject.
Qualquer closure que seja armazenada como propriedade (completion handler, callback) e capture self deve usar [weak self] na lista de captura. A exceção são closures que executam imediatamente e não são armazenadas (sorted, map, filter). Para elas, unowned self é seguro.
Em arquiteturas complexas (VIPER, Coordinators, Redux), rastreie a direção das referências fortes. O proprietário mantém uma referência forte ao subordinado, mas o subordinado deve referenciar o proprietário apenas por meio de weak ou unowned. O fluxo de dados unidirecional simplifica o gerenciamento de referências.
// Exemplo: verificação com registro de deinit
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// Uso: todos os ViewController herdam BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// Ao fechar ProfileVC esperamos "✅ ProfileVC deallocated" no console
Uma classe base com registro de deinit fornece feedback instantâneo. Se a mensagem não aparecer quando a tela deve ser fechada, há um retain cycle nesta classe. Adicione esta prática ao template do projeto para todos os ViewControllers.
Perguntas frequentes
Retain cycle é um problema específico do ARC onde um ciclo fechado de referências fortes bloqueia a liberação. No GC, o coletor analisa a acessibilidade a partir do conjunto raiz, não contadores de referência — portanto, ciclos não são vazamentos. No ARC, no entanto, qualquer ciclo isolado é um vazamento garantido.
Uma referência weak não aumenta o retain count de um objeto. Se você substituir uma das referências fortes em um ciclo por weak, o retain count de cada objeto pode chegar a zero. Após o objeto ser liberado, a referência weak é automaticamente definida como nil, impedindo o acesso à memória liberada.
Sim, um retain cycle pode incluir qualquer número de objetos: A → B → C → A. Para quebrá-lo, você só precisa quebrar um elo no ciclo — substitua qualquer referência forte por weak ou unowned. As ferramentas mostram o grafo completo, não apenas pares de objetos.
GCD (Grand Central Dispatch) não armazena a closure após a execução. O DispatchWorkItem executa e é liberado, mesmo que a closure capture self. Um retain cycle ocorre apenas quando uma closure é armazenada como propriedade (completion handler em uma classe), não quando é passada para uma fila.
Instruments Leaks nem sempre encontra retain cycles temporários (que duram segundos) ou referências cíclicas em objetos C/C++ por meio de bridging. Para uma verificação completa, use Memory Debugger manualmente junto com o registro de deinit de todos os objetos chave na cena.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também