---
title: "LeakCanary no Android com Kotlin: encontre memory leaks em 2026"
url: "https://kotlin.dev.br/blog/leakcanary-android-kotlin-memory-leaks-2026/"
markdown_url: "https://kotlin.dev.br/blog/leakcanary-android-kotlin-memory-leaks-2026.MD"
description: "Use LeakCanary no Android com Kotlin para detectar memory leaks em Activity, Fragment, Compose, ViewModel, coroutines e listeners durante o desenvolvimento."
date: "2026-08-19"
author: "Karina Melo"
---

# LeakCanary no Android com Kotlin: encontre memory leaks em 2026

Use LeakCanary no Android com Kotlin para detectar memory leaks em Activity, Fragment, Compose, ViewModel, coroutines e listeners durante o desenvolvimento.


**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](/blog/baseline-profiles-macrobenchmark-android-kotlin-2026/) e com o [guia de performance Kotlin](/guias/guia-kotlin-performance/).

## 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:

```kotlin
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:

```kotlin
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 é:

1. um objeto entra em estado no qual deveria ser destruído;
2. o `AppWatcher` mantém uma referência fraca para observá-lo;
3. após um período e uma tentativa de garbage collection, o objeto continua alcançável;
4. LeakCanary captura um heap dump do processo;
5. o analisador encontra caminhos de retenção entre GC roots e objetos observados;
6. 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:

```kotlin
dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:<versao-estavel>")
}
```

Com Version Catalog:

```toml
# gradle/libs.versions.toml
[versions]
leakcanary = "<versao-estavel>"

[libraries]
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
```

```kotlin
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](/blog/androidx-startup-kotlin-inicializacao-app-2026/) para auditar o que roda antes da primeira tela.

Depois do setup, execute um cenário simples:

1. abra uma Activity ou Fragment;
2. saia da tela;
3. repita algumas vezes;
4. aguarde a notificação do LeakCanary;
5. abra a análise e procure o objeto marcado como vazando.

## Como ler um leak trace

Um trace conceitual pode parecer com isto:

```text
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.

```kotlin
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:

```kotlin
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

```kotlin
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:

```kotlin
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:

```kotlin
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:

```kotlin
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:

```kotlin
@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`:

```kotlin
@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:

```kotlin
@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](/blog/coil-jetpack-compose-kotlin-2026/) para imagens e [Media3 ExoPlayer com Compose](/blog/media3-exoplayer-compose-kotlin-2026/) 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:

```kotlin
@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:

```kotlin
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:

```kotlin
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](/guias/kotlin-flow-guia/) e o artigo sobre [testes de Flow e StateFlow com Turbine](/blog/testando-flow-stateflow-turbine-kotlin-2026/) 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`:

```kotlin
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:

```kotlin
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](/blog/navigation-compose-navhost-navcontroller-kotlin-2026/).

## Observando objetos próprios com AppWatcher

Você pode pedir ao LeakCanary para verificar um objeto que deveria ser liberado:

```kotlin
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:

1. confirme a versão do Android e da dependência;
2. reproduza em outro API level;
3. pesquise no issue tracker oficial do projeto;
4. atualize para uma versão que contenha a correção;
5. verifique se o leak já é classificado como conhecido pelo LeakCanary;
6. 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:

1. abra a tela;
2. execute a ação relevante;
3. feche a tela;
4. repita o ciclo de cinco a dez vezes;
5. verifique se a quantidade de instâncias antigas cresce;
6. capture o trace;
7. corrija uma referência;
8. 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](/blog/espresso-android-kotlin-testes-ui-2026/) e o [guia de CI/CD para Kotlin](/guias/guia-kotlin-ci-cd/).

## 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](/blog/firebase-crashlytics-anr-android-kotlin-2026/) 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 `DisposableEffect` quando registram recursos;
- [ ] coroutines usam `lifecycleScope`, `viewModelScope` ou outro escopo com dono;
- [ ] collectors de View respeitam `repeatOnLifecycle`;
- [ ] `callbackFlow` chama `awaitClose` para 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](/guias/guia-kotlin-performance/), [Baseline Profiles e Macrobenchmark](/blog/baseline-profiles-macrobenchmark-android-kotlin-2026/), [Espresso no Android](/blog/espresso-android-kotlin-testes-ui-2026/) e [coroutines com Flow](/guias/kotlin-flow-guia/). Juntos, esses guias ajudam a conectar memória, ciclo de vida, testes e experiência real do usuário.

## Fontes oficiais

- [Documentação oficial do LeakCanary](https://square.github.io/leakcanary/)
- [Repositório do LeakCanary no GitHub](https://github.com/square/leakcanary)
- [Visão geral de gerenciamento de memória no Android](https://developer.android.com/topic/performance/memory-overview)
- [Documentação de lifecycle-aware coroutines no Android](https://developer.android.com/topic/libraries/architecture/coroutines)
- [Documentação de side effects no Jetpack Compose](https://developer.android.com/develop/ui/compose/side-effects)
