Resposta rápida: adicione o LeakCanary apenas ao build de debug com debugImplementation. A biblioteca observa objetos Android que deveriam ter sido liberados — como Activity, Fragment, View e ViewModel —, espera a coleta de lixo e, quando o objeto continua retido, captura e analisa um heap dump. Use o leak trace para seguir a cadeia desde uma GC root até o objeto vazado; corrija a referência de vida mais longa que está segurando a referência de vida curta. Em Compose, o problema normalmente não é o composable em si, mas um listener, callback, coroutine, singleton ou objeto lembrado que sobrevive ao ciclo correto. Não publique LeakCanary no release e não trate toda retenção temporária como leak confirmado.
Um memory leak no Android raramente começa como um crash fácil de reproduzir. Primeiro, a navegação fica mais pesada. Depois, imagens, Views e Activities antigas permanecem na memória. Em aparelhos intermediários, o sistema passa a coletar lixo com frequência, o app engasga e, no pior caso, termina com OutOfMemoryError.
O LeakCanary, projeto da Square, automatiza uma investigação que antes exigia capturar heap dumps manualmente e navegar por milhares de objetos. Ele não substitui conhecimento de ciclo de vida, mas reduz o espaço de busca ao mostrar quem está retendo o quê e por qual caminho.
Este guia mostra o setup com Kotlin, como interpretar um leak trace, como investigar vazamentos em Activity, Fragment, View Binding, Compose, coroutines e listeners, além de um fluxo de triagem que evita “corrigir” o sintoma errado. Para medir o impacto geral do app, combine a investigação com Baseline Profiles e Macrobenchmark e com o guia de performance Kotlin.
O que é um memory leak no Android?
Um objeto vira candidato a coleta quando o aplicativo não precisa mais dele e não existe um caminho de referências fortes partindo de uma GC root até esse objeto. GC roots incluem, entre outros elementos, threads ativas, campos estáticos e referências mantidas pela VM.
Existe leak quando um objeto que deveria morrer continua alcançável por uma cadeia de referências. Exemplo clássico:
object SessionTracker {
var currentActivity: Activity? = null
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SessionTracker.currentActivity = this
}
}
Após rotação ou navegação, o singleton continua vivo durante o processo inteiro e mantém a Activity antiga. Como a Activity referencia sua janela, Views, drawables e outros objetos, uma única referência pode reter uma árvore grande.
A correção não é chamar System.gc(). É remover o contrato incorreto:
object SessionTracker {
fun trackScreen(screenName: String) {
// Guarde dados, não a Activity.
}
}
Quando uma API realmente precisa de Context por toda a vida do processo, use applicationContext — mas não faça essa troca automaticamente quando a operação exige tema, janela ou ciclo de vida de Activity.
Como o LeakCanary funciona
O fluxo simplificado é:
- um objeto entra em estado no qual deveria ser destruído;
- o
AppWatchermantém uma referência fraca para observá-lo; - após um período e uma tentativa de garbage collection, o objeto continua alcançável;
- LeakCanary captura um heap dump do processo;
- o analisador encontra caminhos de retenção entre GC roots e objetos observados;
- o app exibe uma notificação e uma tela com o leak trace.
A biblioteca conhece ciclos de vida comuns do Android e observa automaticamente tipos relevantes. Você também pode observar objetos próprios com AppWatcher.objectWatcher.
Um detalhe importante: objeto retido não significa necessariamente leak definitivo. Uma operação assíncrona pode reter algo por alguns segundos e depois liberar. LeakCanary usa uma janela de observação para diminuir falsos positivos, mas você ainda precisa interpretar o contexto e reproduzir o cenário.
Configuração no Gradle Kotlin DSL
Adicione a dependência somente em debug:
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:<versao-estavel>")
}
Com Version Catalog:
# gradle/libs.versions.toml
[versions]
leakcanary = "<versao-estavel>"
[libraries]
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
dependencies {
debugImplementation(libs.leakcanary.android)
}
Confira a release atual no repositório oficial antes de fixar a versão. O uso de debugImplementation evita empacotar o analisador e a interface de diagnóstico no artefato distribuído.
Na configuração padrão, não é necessário criar uma subclasse de Application só para iniciar LeakCanary. A inicialização automática acontece no processo de debug. Se o app tem um caminho de startup complexo, veja também AndroidX App Startup com Kotlin para auditar o que roda antes da primeira tela.
Depois do setup, execute um cenário simples:
- abra uma Activity ou Fragment;
- saia da tela;
- repita algumas vezes;
- aguarde a notificação do LeakCanary;
- abra a análise e procure o objeto marcado como vazando.
Como ler um leak trace
Um trace conceitual pode parecer com isto:
GC Root: Global variable in native code
│
├─ br.dev.exemplo.Analytics singleton
│ Leaking: NO
│ ↓ Analytics.screenListeners
├─ java.util.ArrayList
│ ↓ ArrayList[0]
├─ br.dev.exemplo.ProfileFragment$listener
│ ↓ listener.this$0
╰→ br.dev.exemplo.ProfileFragment
Leaking: YES
Leia de cima para baixo:
- a GC root explica por que a cadeia permanece alcançável;
- os objetos intermediários mostram as referências;
- o objeto final deveria ter sido liberado;
- o sublinhado ou destaque da análise costuma indicar a referência suspeita.
No exemplo, o problema não é “o Fragment ocupa memória”. O singleton Analytics guarda um listener que captura o Fragment. A correção é remover o listener no momento adequado ou desenhar a API para devolver uma inscrição descartável.
class ProfileFragment : Fragment() {
private var subscription: Subscription? = null
override fun onStart() {
super.onStart()
subscription = analytics.addScreenListener(::onScreenEvent)
}
override fun onStop() {
subscription?.dispose()
subscription = null
super.onStop()
}
}
Procure o primeiro ponto em que uma referência de vida longa passa a possuir uma referência de vida curta. Corrigir apenas o último objeto do trace costuma mascarar a arquitetura errada.
Activity vazando por singleton, callback ou classe interna
Activities são alvos frequentes porque carregam uma árvore grande e são recriadas em mudanças de configuração.
Singleton guardando Activity ou View
Evite:
object DialogManager {
lateinit var anchor: View
}
Um singleton não deve possuir uma View de uma tela. Passe a View apenas durante a chamada ou mantenha a coordenação no escopo da própria Activity/Fragment.
Callback que nunca é removido
class LocationRepository {
private val callbacks = mutableSetOf<(Location) -> Unit>()
fun observe(callback: (Location) -> Unit) {
callbacks += callback
}
}
Se a Activity registra uma lambda e não consegue cancelar, o repository pode retê-la. Prefira uma API com cancelamento, Flow bem escopado ou observação ligada ao lifecycle.
Handler e tarefas atrasadas
Uma tarefa postada pode capturar a Activity:
handler.postDelayed({ renderSuccess() }, 60_000)
Se a espera pertence à tela, remova callbacks ao destruí-la ou modele a operação com uma coroutine no escopo apropriado. Não use uma thread global para trabalho de UI.
Fragment e View Binding: respeite dois ciclos de vida
Um Fragment pode continuar vivo depois que sua View foi destruída. Por isso, guardar binding até onDestroy() vaza a árvore visual durante o período em que apenas o Fragment deveria existir.
Padrão correto:
class CheckoutFragment : Fragment(R.layout.fragment_checkout) {
private var _binding: FragmentCheckoutBinding? = null
private val binding get() = requireNotNull(_binding)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentCheckoutBinding.bind(view)
binding.confirmButton.setOnClickListener {
confirmarPedido()
}
}
override fun onDestroyView() {
binding.confirmButton.setOnClickListener(null)
_binding = null
super.onDestroyView()
}
}
O ponto central é limpar referências à View em onDestroyView(), não apenas quando o Fragment é destruído. Adapters, animações, callbacks de mapas, WebViews e listeners também podem precisar de teardown nesse momento.
Se um adapter mantém callbacks para o Fragment, considere soltá-lo do RecyclerView:
override fun onDestroyView() {
binding.items.adapter = null
_binding = null
super.onDestroyView()
}
Isso não é obrigatório em toda lista. Aplique quando o adapter ou seus ViewHolders retêm recursos ligados à View e o trace aponta esse caminho.
LeakCanary com Jetpack Compose
Compose reduz parte dos problemas ligados a Views, mas não elimina vazamentos. Composables entram e saem da composição; efeitos precisam registrar e liberar recursos de maneira simétrica.
Listener sem DisposableEffect
Errado:
@Composable
fun ConnectivityBanner(monitor: ConnectivityMonitor) {
monitor.addListener { connected ->
// Atualiza estado.
}
}
A recomposição pode registrar múltiplos listeners, e o monitor pode reter callbacks antigos. Use DisposableEffect:
@Composable
fun ConnectivityBanner(monitor: ConnectivityMonitor) {
var connected by remember { mutableStateOf(false) }
DisposableEffect(monitor) {
val subscription = monitor.addListener { connected = it }
onDispose { subscription.dispose() }
}
if (!connected) {
Text("Sem conexão")
}
}
Objeto pesado lembrado no lugar errado
remember conserva um valor enquanto aquela posição permanece na composição. Ele não é um cache global nem substitui ViewModel. Se o objeto possui recursos, sockets, players ou callbacks, defina claramente quem fecha tudo.
Para um recurso que precisa de Context, evite capturar uma Activity quando o ciclo esperado é maior:
@Composable
fun rememberImageLoader(): ImageLoader {
val context = LocalContext.current.applicationContext
return remember(context) {
ImageLoader.Builder(context).build()
}
}
Nem todo objeto deve ser criado assim; bibliotecas como Coil já oferecem integração própria. O princípio é alinhar a vida do recurso ao dono correto. Consulte Coil com Jetpack Compose para imagens e Media3 ExoPlayer com Compose para players que exigem liberação explícita.
Lambdas antigas e rememberUpdatedState
Um efeito longo pode capturar uma callback desatualizada. Isso não é sempre um leak, mas cria retenção e comportamento incorreto. Quando o efeito deve continuar e usar a callback mais recente:
@Composable
fun TimeoutEffect(onTimeout: () -> Unit) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(5_000)
currentOnTimeout()
}
}
Coroutines e Flow podem causar memory leak?
Coroutines não vazam memória automaticamente. O risco aparece quando o escopo vive mais que a tela ou quando um collector mantém referências por tempo indeterminado.
Evite escopo sem dono:
class ProfileActivity : AppCompatActivity() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
}
Se esse escopo não for cancelado, o job pode continuar retendo a Activity. Prefira lifecycleScope, viewLifecycleOwner.lifecycleScope ou viewModelScope, conforme a responsabilidade.
Para coletar Flow apenas quando a View do Fragment está ativa:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
render(state)
}
}
}
repeatOnLifecycle cancela o bloco quando o lifecycle sai do estado e reinicia quando volta. Isso também evita trabalho de UI fora da tela. O guia de Kotlin Flow e o artigo sobre testes de Flow e StateFlow com Turbine aprofundam o tema.
Cuidado com GlobalScope, escopos em object, callbacks convertidos incorretamente com callbackFlow e jobs guardados em singletons. Se usar callbackFlow, sempre desregistre a fonte em awaitClose:
fun ConnectivityMonitor.status(): Flow<Boolean> = callbackFlow {
val callback = register { connected -> trySend(connected) }
awaitClose { unregister(callback) }
}
ViewModel também pode vazar Activity
O ViewModel foi feito para sobreviver a mudanças de configuração. Logo, ele não deve guardar Activity, Fragment, View, binding ou NavController.
Evite:
class CheckoutViewModel(
private val activity: Activity,
) : ViewModel()
O ViewModel deve emitir estado e eventos; a camada de UI decide como navegar, abrir diálogo ou acessar recursos visuais.
Quando contexto de aplicação é inevitável para uma API Android, AndroidViewModel existe, mas frequentemente uma dependência mais estreita — repository, resource provider ou interface — deixa o código mais testável. Para estado e navegação em Compose, veja Navigation Compose com NavHost e NavController.
Observando objetos próprios com AppWatcher
Você pode pedir ao LeakCanary para verificar um objeto que deveria ser liberado:
import leakcanary.AppWatcher
class CheckoutCoordinator {
fun close() {
AppWatcher.objectWatcher.expectWeaklyReachable(
watchedObject = this,
description = "CheckoutCoordinator depois de close()",
)
}
}
Use isso em componentes com ciclo de vida próprio: coordinator, presenter legado, controller de feature, sessão temporária ou cache descartável. A chamada deve ocorrer quando o objeto deveria ficar fracamente alcançável.
Não espalhe observação em cada classe. Comece por objetos relevantes que não são conhecidos automaticamente pelo Android e cuja retenção apareceu em métricas ou inspeção.
Leaks de bibliotecas e referências conhecidas
Às vezes o trace termina em código do framework Android, de fabricante ou de uma biblioteca. Antes de criar uma correção improvisada:
- confirme a versão do Android e da dependência;
- reproduza em outro API level;
- pesquise no issue tracker oficial do projeto;
- atualize para uma versão que contenha a correção;
- verifique se o leak já é classificado como conhecido pelo LeakCanary;
- crie workaround somente com evidência e comentário.
Não ignore um trace só porque passa por uma classe de framework. Seu código pode ter deixado de executar o teardown necessário. Da mesma forma, não refatore todo o app para contornar uma retenção pequena e temporária já conhecida no sistema.
Como reproduzir e priorizar um leak
Uma análise isolada ajuda, mas a prioridade depende do impacto. Use um roteiro repetível:
- abra a tela;
- execute a ação relevante;
- feche a tela;
- repita o ciclo de cinco a dez vezes;
- verifique se a quantidade de instâncias antigas cresce;
- capture o trace;
- corrija uma referência;
- repita exatamente o mesmo cenário.
Priorize leaks que:
- retêm Activity, Fragment ou árvore de Views inteira;
- acumulam uma instância a cada navegação;
- seguram bitmaps, vídeo, mapas ou buffers grandes;
- permanecem durante toda a sessão;
- afetam fluxos frequentes;
- reproduzem em aparelhos com pouca memória.
Uma referência retida por dois segundos durante uma transição tem impacto diferente de dez Activities de checkout acumuladas após cada compra.
LeakCanary deve rodar no CI?
A experiência principal do LeakCanary é interativa no debug. Heap dumps são caros, dependem do cenário e podem tornar testes instrumentados lentos. Ainda assim, times maduros podem criar verificações direcionadas para regressões críticas, especialmente com as APIs de análise e instrumentação do ecossistema.
Antes de automatizar, estabilize o básico:
- cenários instrumentados determinísticos;
- emulador e API level fixados;
- dados controlados;
- navegação que encerra a tela de fato;
- política clara para referências conhecidas;
- armazenamento do heap dump como artefato quando houver falha.
Não transforme qualquer retenção em bloqueio global do pipeline sem triagem. Uma estratégia melhor é começar por fluxos que já tiveram leaks graves e executar essa suíte em job separado. Para organizar testes instrumentados, consulte o guia de Espresso com Kotlin e o guia de CI/CD para Kotlin.
LeakCanary, profiler e métricas de produção
Cada ferramenta responde a uma pergunta:
| Ferramenta | Melhor uso |
|---|---|
| LeakCanary | descobrir automaticamente objetos retidos no desenvolvimento |
| Memory Profiler | observar alocações, heap e instâncias manualmente |
| Heap dump + analisador | investigação profunda e comparação de dominadores |
| Macrobenchmark | medir startup, frame timing e cenários de performance |
| Crash reporting | acompanhar OutOfMemoryError, ANR e impacto em produção |
LeakCanary encontra cadeias de retenção, mas não prova sozinho que um lag veio daquele leak. Combine evidências. Se a memória cresce sem objetos Android vazados, investigue caches sem limite, bitmaps, buffers nativos, WebView, banco, imagens ou alocação excessiva.
O guia de Firebase Crashlytics, ANRs e logs ajuda a fechar o ciclo entre diagnóstico local e sintomas vistos por usuários.
Erros comuns ao usar LeakCanary
Adicionar a dependência em implementation
Isso pode levar código e recursos de diagnóstico ao release. Use debugImplementation.
Limpar a referência errada
Zerar uma variável no objeto vazado não resolve quando um singleton externo continua guardando um listener. Siga o trace até a relação de posse incorreta.
Usar applicationContext como cura universal
Ele serve para dependências ligadas ao processo. APIs de UI precisam de contexto temático ou Activity. Mudar o tipo sem entender a responsabilidade pode criar bugs visuais ou de navegação.
Cancelar todas as coroutines em qualquer callback
Cancele o escopo que pertence àquele dono. Um ViewModel pode continuar após rotação; um collector da View não deve. Escolher o lifecycle errado perde trabalho legítimo ou mantém trabalho inútil.
Ignorar onDestroyView()
No Fragment, a View tem vida menor. Binding, adapter e listeners ligados à árvore devem seguir o ciclo da View.
Chamar GC manualmente em produção
A coleta é responsabilidade da runtime. Chamar GC não quebra cadeias de referências fortes e pode introduzir pausas.
Aceitar um leak porque “só acontece no debug”
LeakCanary torna o problema visível no debug; isso não significa que a referência desaparece no release. Confirme se a retenção vem da própria instrumentação ou do app.
Checklist de investigação
- LeakCanary está em
debugImplementation; - o cenário de navegação é reproduzível;
- o objeto realmente deveria ter sido destruído;
- o trace foi lido desde a GC root;
- singletons não guardam Activity, Fragment ou View;
- listeners e callbacks possuem cancelamento simétrico;
- binding de Fragment é limpo em
onDestroyView(); - adapters e recursos visuais são liberados quando necessário;
- efeitos Compose usam
DisposableEffectquando registram recursos; - coroutines usam
lifecycleScope,viewModelScopeou outro escopo com dono; - collectors de View respeitam
repeatOnLifecycle; -
callbackFlowchamaawaitClosepara desregistrar callbacks; - ViewModel não guarda objetos de UI;
- a correção foi validada repetindo o mesmo cenário;
- leaks de framework ou biblioteca foram conferidos em fontes oficiais.
Perguntas frequentes
LeakCanary funciona com Jetpack Compose?
Sim. Ele observa objetos do processo e analisa o heap independentemente de a UI usar Views ou Compose. Em Compose, investigue efeitos sem descarte, callbacks, coroutines, players, objetos lembrados e dependências que capturam Activity.
LeakCanary deixa o app lento?
A captura e a análise de heap têm custo perceptível, por isso a ferramenta deve ficar no debug. Esse custo aparece quando há objetos retidos e uma análise é disparada; ele não representa a performance normal do release.
Todo objeto retido é um memory leak?
Não. Pode existir retenção temporária legítima. Considere o ciclo de vida esperado, o tempo, a repetição e se o número de instâncias cresce a cada uso.
Preciso chamar LeakCanary manualmente na Application?
Na configuração padrão, não. Adicionar a dependência de debug normalmente ativa a instalação automática. Customizações avançadas devem seguir a documentação da versão adotada.
Como testar se uma Activity foi liberada?
Abra e feche a Activity repetidamente, aguarde a janela de observação e analise a notificação. Para regressões específicas, crie um cenário instrumentado controlado e use as APIs de observação/análise compatíveis com sua versão.
WeakReference resolve memory leak?
Às vezes ela modela uma relação não proprietária, mas raramente deve ser a primeira correção. O melhor é remover a referência desnecessária, oferecer cancelamento ou alinhar o objeto ao ciclo de vida correto. Espalhar WeakReference pode esconder um contrato ruim e introduzir null imprevisível.
Conclusão
LeakCanary transforma um problema abstrato — “a memória parece crescer” — em uma cadeia concreta de referências. O valor da ferramenta não está apenas na notificação, e sim na pergunta que o trace força o time a responder: qual objeto de vida longa está possuindo algo que já deveria ter morrido?
No Android com Kotlin, as correções mais frequentes são simples depois que a cadeia fica visível: não guardar Activity em singleton, remover listeners, limpar binding em onDestroyView(), cancelar trabalho no escopo certo e usar DisposableEffect em integrações Compose. O difícil é manter disciplina de ciclo de vida em APIs compartilhadas.
Adicione LeakCanary ao debug, escolha um fluxo que navega por telas pesadas e repita o cenário. Corrija primeiro leaks acumulativos de Activity, Fragment, imagens e players. Depois, use Profiler, benchmarks e métricas de produção para tratar caches e alocações que não aparecem como retenções clássicas.
Para continuar a trilha, leia performance em Kotlin, Baseline Profiles e Macrobenchmark, Espresso no Android e coroutines com Flow. Juntos, esses guias ajudam a conectar memória, ciclo de vida, testes e experiência real do usuário.