Congelamento (travamento) é um estado no qual um aplicativo móvel para de responder a qualquer ação do usuário por um período prolongado. Ao contrário de lags (lentidão) e glitches (comportamento incorreto), o congelamento bloqueia completamente a UI: toques não são processados, a animação para, a tela “congela”. A causa é o bloqueio da thread principal por uma operação síncrona, um deadlock em código multithread ou uma coleta de lixo anormalmente longa. De acordo com a Documentação do Apple Main Thread Checker, mais de 40% dos relatórios de crash no iOS estão relacionados ao bloqueio da thread principal. No Android, uma situação semelhante leva ao ANR — o diálogo do sistema “O aplicativo não responde”.
Principais pontos
Congelamento (travamento) em um aplicativo móvel é um estado no qual o aplicativo para de processar eventos de entrada e atualizar a interface por vários segundos ou mais. Tecnicamente, isso significa que a thread principal está bloqueada e não pode executar a próxima iteração do loop de execução.
Um lag é um atraso de até 500 ms no qual o usuário nota lentidão, mas o aplicativo continua funcionando. Congelamento dura de 1 segundo a dezenas de segundos. ANR no Android é um caso especial de congelamento que durou mais de 5 segundos e foi detectado pelo sistema. Nem todo congelamento leva a ANR, mas todo ANR é um congelamento documentado pelo sistema.
No Android, um congelamento com mais de 5 segundos provoca um diálogo ANR oferecendo fechar o aplicativo. No iOS, o sistema tem um watchdog — se o aplicativo não responder a eventos por 10–20 segundos, o Watchdog encerra o processo com o código 0x8badf00d (ate bad food). O usuário vê apenas o aplicativo fechando repentinamente para a tela inicial.
Qualquer operação que leve mais de 100 ms e seja executada na thread principal pode potencialmente causar um congelamento. Vamos ver as principais fontes de bloqueios.
Ler um arquivo grande, uma requisição de rede sem assincronia, salvar dados no SharedPreferences com o método síncrono apply seguido de commit — todas essas operações bloqueiam a thread principal. No Android, ler sincronamente um arquivo de 10 MB pode levar 200–500 ms dependendo da velocidade da memória flash. No iOS, um carregamento síncrono de URLSession sem completionHandler bloqueia a UI pelo tempo de resposta do servidor.
Quando duas threads esperam por recursos retidos uma pela outra, ocorre um deadlock. Em aplicativos móveis, um cenário típico é a thread A bloquear Lock1 e esperar Lock2, enquanto a thread B bloqueia Lock2 e espera Lock1. Ambas as threads congelam para sempre. Se uma delas for a thread principal, o aplicativo congela completamente.
Um erro de lógica — por exemplo, while(true) sem condição de saída ou recursão sem caso base — leva à execução infinita na thread principal. O Android detecta isso via ANR após 5 segundos, o iOS — via Stackshot, que captura uma pilha de chamadas infinitamente repetida.
O diagnóstico de congelamentos requer ferramentas capazes de capturar o estado de todas as threads no momento do bloqueio.
Em cada ANR, o sistema Android salva um arquivo /data/anr/traces.txt contendo um dump de pilha de cada thread do aplicativo. Analisar este arquivo é o principal método de diagnóstico: encontre a thread main e veja em qual método ela parou. Se a pilha terminar em Thread.sleep, InputStream.read ou Lock.lock — a causa foi encontrada.
O Xcode pode capturar um Stackshot — uma snapshot das pilhas de todas as threads — quando o aplicativo congela (sinal SIGSTOP). Ative “Logging” → “Include Stackshot Logs” no esquema. Em um crash com código 0x8badf00d, extraia o log de crash de Devices & Simulators e encontre a thread com.apple.main-thread com a pilha travada.
O Main Thread Checker detecta automaticamente chamadas UIKit de threads em segundo plano enquanto o aplicativo está em execução. Ative-o no esquema (Diagnostics → Main Thread Checker). Cada aviso é uma causa potencial de congelamento, especialmente se ocorrer em um closure completionHandler de uma requisição de rede.
Exemplo de detecção de bloqueios via StrictMode no Android:
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
Eliminar congelamentos começa com a movimentação de todas as operações potencialmente longas para threads em segundo plano. Vamos ver técnicas específicas para cada plataforma.
O Kotlin Coroutines com viewModelScope.launch(Dispatchers.IO) garante que operações de rede ou leituras de banco de dados sejam executadas em uma thread em segundo plano. Dispatchers.Main é usado apenas para atualizações de UI. Importante: todas as funções suspend devem ser estruturadas — as corrotinas filhas são canceladas quando a pai é cancelada, prevenindo vazamentos de threads.
O Grand Central Dispatch com DispatchQueue.global(qos: .userInitiated) para tarefas em segundo plano e DispatchQueue.main.async para atualizações de UI é o padrão. Evite sync() na fila principal — isso é um deadlock garantido. Use async/await (Swift 5.5+) para código assíncrono mais legível com retorno automático à thread principal via MainActor.
Blocos synchronized em Kotlin e @synchronized em Swift na thread principal são perigosos: se outra thread já adquiriu este bloqueio, a thread principal vai travar esperando. Use tipos atômicos (AtomicInteger, propriedades atômicas em Swift) ou filas seriais em vez de bloqueios.
Exemplo de carregamento assíncrono de dados com corrotinas no Android:
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
Uma combinação de ferramentas, princípios arquitetônicos e processos de revisão de código ajuda a prevenir sistematicamente congelamentos.
Configure o StrictMode com penaltyDeath para as políticas de thread — isso causará uma falha imediata do aplicativo quando uma chamada de rede ou E/S de disco for detectada na thread principal. O desenvolvedor não poderá ignorar o problema. Em builds de produção, use penaltyLog para coletar estatísticas sem falhas.
No iOS, ative o Main Thread Checker no esquema Debug e configure a CI para executar testes com esta opção. Se um teste contiver uma chamada UIKit de uma thread em segundo plano — ele deve falhar. Esta é a única maneira confiável de identificar o problema antes de enviar para o TestFlight.
Adicione um item obrigatório ao processo de revisão de código: verificar se qualquer chamada de rede, operação de arquivo, acesso a banco de dados ou cálculo pesado é executado em uma thread em segundo plano. O Deadlock pode ser detectado com analisadores estáticos: o Infer do Facebook e o Thread Safety Checker do Xcode encontram bloqueios potenciais antes da execução.
Perguntas frequentes
ANR (Application Not Responding) é uma notificação do sistema Android que aparece quando a thread principal congela por mais de 5 segundos. Congelamento é um conceito mais amplo: qualquer bloqueio de UI de qualquer duração. O iOS não tem ANR, mas tem um Watchdog com tempo limite de 10–20 segundos.
O arquivo está localizado em /data/anr/traces.txt. O acesso requer acesso root ou adb shell: execute adb shell cat /data/anr/traces.txt \> traces.txt com privilégios root. Na pilha, encontre a thread “main” — o último método chamado indica a causa do bloqueio.
Se o congelamento durar menos de 10 segundos, o Watchdog não é acionado e o aplicativo simplesmente “trava” até que a operação bloqueante seja concluída. O usuário não vê um crash, mas sente frustração. Para detectar tais casos, use o MetricKit com traces personalizados de tempo de execução.
Use testes de UI com verificação de que a tela abre em < 1 segundo. Adicione à CI a medição do tempo entre o toque e o aparecimento da próxima tela. No Android, use Espresso com IdlingResource para aguardar operações assíncronas. No iOS, use XCTest com XCTWaiter para verificar o tempo de carregamento.
O SwiftUI por si só não causa congelamentos, mas cálculos complexos na propriedade body sim. Se o body levar 500 ms para calcular devido a operações pesadas, a UI congela. A solução é descarregar os cálculos para Task.detached e atualizar @State de forma assíncrona no ator principal.
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