Depurando Aplicativos Inativos
Outras línguas: English 中文 Español
Existem muitos tutoriais de depuradores por aí que ensinam a configurar pontos de interrupção de linha, registrar valores ou avaliar expressões. Enquanto este conhecimento por si só proporciona muitas ferramentas para depurar seu aplicativo, cenários do mundo real podem ser um pouco mais complexos e exigir uma abordagem mais avançada.
![Post banner](/img/debug-unresponsive-apps-banner.png)
Neste artigo, aprenderemos como localizar códigos que causam um congelamento de UI sem muito conhecimento prévio de projeto e corrigir códigos defeituosos em tempo real.
O Problema
Se você quiser seguir, comece clonando este repositório: https://github.com/flounder4130/debugger-example
Suponha que você tenha um aplicativo complexo que trava quando você realiza alguma ação. Você sabe como reproduzir o bug, mas a dificuldade é que você não sabe qual parte do código é responsável por esta funcionalidade.
![A UI do aplicativo de amostra tem muitos botões para realizar algumas ações](/img/sophisticated-app-1.png)
Em nosso aplicativo de exemplo, a suspensão ocorre quando você clica no Button N. No entanto, não é tão fácil encontrar o código responsável por essa ação:
![Procurar 'Button N' em todo o projeto não retorna nenhum resultado](/img/find-in-files.png)
![Procurar 'Button N' em todo o projeto não retorna nenhum resultado](/img/find-in-files-dark.png)
Vamos ver como podemos usar o depurador para encontrá-lo.
Pontos de Interrupção de Método
A vantagem dos pontos de interrupção de método sobre os pontos de interrupção de linha é que eles podem ser usados em hierarquias inteiras de classes. Como isso é útil no nosso caso?
Se você olhar para o projeto de exemplo, verá que todas as classes de ação são derivadas da
interface Action
com um
único método: perform()
.
![Ícone do ponto de interrupção do método no editor de calha](/img/method-breakpoint-in-interface.png)
![Ícone do ponto de interrupção do método no editor de calha](/img/method-breakpoint-in-interface-dark.png)
Definir um ponto de interrupção de método neste método de interface irá suspender a aplicação sempre que um dos métodos derivados for chamado. Para definir um ponto de interrupção de método, clique na linha que declara o método.
Inicie a sessão do depurador e clique em Button N. A aplicação é suspensa em ActionImpl14
.
Agora sabemos onde o código correspondente a este botão está localizado.
![O aplicativo foi suspenso em uma classe que implementa a interface de ação](/img/suspended-in-implementing-class.png)
![O aplicativo foi suspenso em uma classe que implementa a interface de ação](/img/suspended-in-implementing-class-dark.png)
Embora neste artigo estejamos focados em encontrar o bug, essa técnica também pode economizar muito tempo quando você quer entender como algo funciona em uma base de código grande.
Pause o Aplicativo
A abordagem com pontos de bloqueio de método funciona bem, mas é baseada na suposição de que sabemos algo sobre a interface pai. E se essa suposição estiver errada, ou não pudermos usar essa abordagem por algum outro motivo?
Bem, nós podemos fazer isso mesmo sem os pontos de interrupção. Clique em Button N, e enquanto o aplicativo está travado, vá para o IntelliJ IDEA. No menu principal, selecione Run | Debugging Actions | Pause Program.
![A pilha de chamadas para o thread principal mostra o que ele está fazendo atualmente](/img/examine-threads-in-paused-app.png)
![A pilha de chamadas para o thread principal mostra o que ele está fazendo atualmente](/img/examine-threads-in-paused-app-dark.png)
O aplicativo será suspenso, permitindo-nos examinar o estado atual dos threads na aba Threads & Variables. Isto nos dá uma compreensão do que o aplicativo está fazendo no momento. Como ele está travado, podemos identificar o método de travamento e rastrear de volta ao local da chamada.
Esta abordagem tem algumas vantagens sobre um dump de thread mais tradicional, que abordaremos em breve. Por exemplo, ele fornece informações sobre variáveis de uma forma conveniente e permite que você controle a execução adicional do programa.
Para mais dicas e truques com Pause Program veja Depurar sem Pontos de Interrupção e Debugger.godMode().
Dumps de Thread
Finalmente, podemos usar um dump de thread, que não é estritamente um recurso do depurador. Ele está disponível independentemente de você estar usando o depurador ou não.
Clique em Button N. Enquanto o aplicativo está travado, vá para o IntelliJ IDEA. No menu principal, selecione Run | Debugging Actions | Get Thread Dump.
Examine os threads disponíveis à esquerda, e em AWT-EventQueue você verá o que está causando o problema.
![Visualizador de dump de thread no IntelliJ IDEA](/img/ij-thread-dump.png)
![Visualizador de dump de thread no IntelliJ IDEA](/img/ij-thread-dump-dark.png)
A desvantagem dos dumps de thread é que eles só fornecem um snapshot do estado do programa no momento em que foram feitos. Você não pode usar dumps de thread para explorar variáveis ou controlar a execução do programa.
Em nosso exemplo, não precisamos recorrer a um dump de thread. No entanto, eu ainda queria mencionar esta técnica pois ela pode ser útil em outros casos, como quando você está tentando depurar um aplicativo que foi lançado sem o agente de depuração.
Entendendo o Problema
Independentemente da técnica de depuração, chegamos a ActionImpl14
.
Nesta classe, alguém pretendia realizar o trabalho em um
thread separado, mas confundiu
Thread.start()
com Thread.run()
,
que executa o código no mesmo thread que o código de chamada.
O analisador estático do IntelliJ IDEA até nos adverte sobre isso no momento do design:
![A análise estática do IntelliJ IDEA dá um aviso sobre uma chamada suspeita para Thread.run()](/img/thread-run-warning.png)
![A análise estática do IntelliJ IDEA dá um aviso sobre uma chamada suspeita para Thread.run()](/img/thread-run-warning-dark.png)
Um método que faz um trabalho pesado (ou um sono pesado neste caso) é chamado no thread da UI e o bloqueia até que o método termine. É por isso que não podemos fazer nada na UI por algum tempo após clicar em Button N.
HotSwap
Agora que descobrimos a causa do bug, vamos corrigir o problema.
Poderíamos parar o programa, recompilar o código e depois executá-lo novamente. No entanto, nem sempre é conveniente redesdobrar todo o aplicativo apenas por causa de uma pequena mudança.
Vamos fazer isso de uma maneira inteligente. Primeiro, corrija o código usando o quick-fix sugerido:
![O menu de contexto (Alt-Enter) oferece uma opção para corrigir o código suspeito](/img/quick-fix-replace-thread-run.png)
![O menu de contexto (Alt-Enter) oferece uma opção para corrigir o código suspeito](/img/quick-fix-replace-thread-run-dark.png)
Depois que o código estiver pronto, clique em Run | Debugging Actions | Reload Changed Classes. Uma mensagem aparece, confirmando que o novo código chegou à VM.
![Uma mensagem confirma que as classes atualizadas chegaram ao tempo de execução](/img/hotswap-balloon.png)
![Uma mensagem confirma que as classes atualizadas chegaram ao tempo de execução](/img/hotswap-balloon-dark.png)
Vamos voltar para o aplicativo e verificar. Clicar em Button N não trava mais o aplicativo.
Lembre-se que o HotSwap tem suas limitações. Se você está interessado em capacidades extendidas do HotSwap, pode ser uma boa ideia dar uma olhada em ferramentas avançadas como DCEVM ou JRebel.
Resumo
Usando nosso raciocínio e alguns recursos do depurador, fomos capazes de localizar o código que estava causando um congelamento da UI em nosso projeto. Em seguida, procedemos para corrigir o código sem perder tempo com recompilação e redesdobramento, o que pode ser demorado em projetos do mundo real.
Espero que você ache as técnicas descritas úteis. Deixe-me saber o que você pensa!
Fique atento para mais!