Como manter seu app responsivo

Figura 1. Uma caixa de diálogo de ANR exibida para o usuário.

É possível escrever um código que passe em todos os testes de desempenho do mundo, mas ainda assim pareça lento, travando ou congelando por períodos significativos ou demorando demais para processar a entrada. O pior que pode acontecer com a capacidade de resposta do seu app é a exibição de a caixa de diálogo "O app não está respondendo" (ANR, na sigla em inglês).

No Android, o sistema protege contra aplicativos que não respondem de maneira suficiente por determinado período exibindo uma caixa de diálogo para informar que seu app parou de responder, como a caixa exibida na Figura 1. Nesse ponto, seu app deixou de responder por um período considerável e, por isso, o sistema oferece ao usuário a opção de sair do app. É fundamental projetar a capacidade de resposta do app para que o sistema nunca exiba uma caixa de diálogo de ANR para o usuário.

Este documento descreve como o sistema Android determina se um aplicativo deixou de responder e oferece diretrizes para garantir que ele mantenha uma capacidade de resposta constante.

O que aciona uma mensagem de ANR?

Geralmente, o sistema exibe uma mensagem de ANR quando o aplicativo não responde à entrada do usuário. Por exemplo, se o aplicativo fica bloqueado em alguma operação de E/S (em geral um acesso à rede) no thread de IU, de modo que o sistema não consiga processar eventos de entrada do usuário. Ou talvez o app gaste muito tempo criando uma estrutura elaborada na memória ou calculando o próximo movimento em um jogo no thread de IU. É sempre importante garantir que esses cálculos sejam eficientes, mas mesmo o código mais eficaz demora para ser executado.

Em qualquer situação em que seu app executar uma operação possivelmente demorada, não realize o trabalho no thread de IU. Em vez disso, crie um thread de trabalho e realize a maioria das operações nele. Isso mantém o thread de IU (que aciona o loop de eventos da interface do usuário) em execução e evita que o sistema conclua que o código congelou. Como esse threading geralmente é realizado no nível da classe, você pode pensar na capacidade de resposta como um problema de classe. Compare essa situação com o desempenho básico do código, que é uma preocupação no nível do método.

No Android, a capacidade de resposta do aplicativo é monitorada pelos serviços de sistema Gerenciador de atividades e Gerenciador de janelas. O Android exibirá a caixa de diálogo de ANR para um determinado aplicativo quando detectar uma das seguintes condições:

  • Nenhuma resposta a um evento de entrada (como eventos de pressionamento de tecla ou toque na tela) em até cinco segundos.
  • Um BroadcastReceiver não concluiu a execução em 10 segundos.

Como evitar ANRs

Por padrão, os aplicativos Android costumam ser executados inteiramente em um único thread, o "thread de IU" ou "thread principal". Isso significa que qualquer ação do seu aplicativo no thread de IU que leve tempo demais para ser concluída pode acionar a caixa de diálogo de ANR porque seu aplicativo não está dando a si mesmo uma chance de manipular as transmissões de evento ou intent de entrada.

Portanto, qualquer método executado no thread de IU deve fazer o mínimo possível de trabalho nesse thread. Em particular, as atividades devem fazer o mínimo possível para configurar os métodos do ciclo de vida na chave, como onCreate() e onResume(). Operações possivelmente longas, como as de rede ou banco de dados, ou cálculos computacionalmente caros, como o redimensionamento de bitmaps, devem ser feitos em um thread de trabalho (ou, no caso de operações de bancos de dados, por meio de uma solicitação assíncrona).

A maneira mais eficaz de criar um thread de trabalho para operações mais longas é usar a classe AsyncTask. Basta estender AsyncTask e implementar o método doInBackground() para executar o trabalho. Para postar alterações de progresso para o usuário, você pode chamar publishProgress(), que chama o método de callback onProgressUpdate(). Na sua implementação de onProgressUpdate() (que é executada no thread de IU), você pode notificar o usuário. Por exemplo:

Kotlin

    private class DownloadFilesTask : AsyncTask<URL, Int, Long>() {

        // Do the long-running work in here
        override fun doInBackground(vararg urls: URL): Long? {
            val count: Float = urls.size.toFloat()
            var totalSize: Long = 0
            urls.forEachIndexed { index, url ->
                totalSize += Downloader.downloadFile(url)
                publishProgress((index / count * 100).toInt())
                // Escape early if cancel() is called
                if (isCancelled) return totalSize
            }
            return totalSize
        }

        // This is called each time you call publishProgress()
        override fun onProgressUpdate(vararg progress: Int?) {
            setProgressPercent(progress.firstOrNull() ?: 0)
        }

        // This is called when doInBackground() is finished
        override fun onPostExecute(result: Long?) {
            showNotification("Downloaded $result bytes")
        }
    }
    

Java

    private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
        // Do the long-running work in here
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
                // Escape early if cancel() is called
                if (isCancelled()) break;
            }
            return totalSize;
        }

        // This is called each time you call publishProgress()
        protected void onProgressUpdate(Integer... progress) {
            setProgressPercent(progress[0]);
        }

        // This is called when doInBackground() is finished
        protected void onPostExecute(Long result) {
            showNotification("Downloaded " + result + " bytes");
        }
    }
    

Para executar esse thread de trabalho, basta criar uma instância e chamar execute():

Kotlin

    DownloadFilesTask().execute(url1, url2, url3)
    

Java

    new DownloadFilesTask().execute(url1, url2, url3);
    

Embora seja mais complicado que AsyncTask, é mais interessante fazer isso em vez de criar a própria classe Thread ou HandlerThread. Se seguir essa opção, defina a prioridade do thread como prioridade de "segundo plano" chamando Process.setThreadPriority() e passando THREAD_PRIORITY_BACKGROUND. Se você não definir o thread como uma prioridade mais baixa dessa maneira, seu app ainda poderá ficar lento, porque ele opera, por padrão, na mesma prioridade do thread de IU.

Se você implementar Thread ou HandlerThread, verifique se o thread de IU não é bloqueado enquanto aguarda a conclusão do thread de trabalho. Não chame Thread.wait() nem Thread.sleep(). Em vez de ficar bloqueado enquanto aguarda a conclusão de um thread de trabalho, o thread principal deve fornecer um Handler a ser postado novamente pelos outros threads após a conclusão. Projetar seu aplicativo dessa maneira permitirá que o thread de IU dele permaneça responsivo à entrada e, assim, evite caixas de diálogo de ANR causadas pelo tempo limite do evento de entrada de cinco segundos.

A restrição específica do tempo de execução de BroadcastReceiver enfatiza o que os broadcast receivers devem fazer: pequenas quantidades discretas de trabalho em segundo plano, como salvar uma configuração ou registrar uma Notification. Assim como com outros métodos chamados no thread de IU, os aplicativos devem evitar operações ou cálculos possivelmente longos em um broadcast receiver. Mas, em vez de executar tarefas intensivas por meio de threads de trabalho, seu aplicativo deverá iniciar um IntentService se for preciso realizar uma ação possivelmente longa em resposta a uma transmissão de intent.

Outro problema comum com objetos BroadcastReceiver ocorre quando eles são executados com muita frequência. A execução frequente em segundo plano pode reduzir a quantidade de memória disponível para outros apps. Para mais informações sobre como ativar e desativar objetos BroadcastReceiver com eficiência, consulte Manipulação de broadcast receivers sob demanda.

Dica: você pode usar o StrictMode para ajudar a localizar operações possivelmente longas, como as de rede ou de banco de dados, que podem estar sendo realizadas acidentalmente no thread principal.

Reforçar a capacidade de resposta

Geralmente, 100 a 200 ms é o limite além do qual os usuários perceberão lentidão em um aplicativo. Sendo assim, veja mais algumas dicas além do que você deve fazer para evitar mensagens de ANR e fazer com que seu aplicativo seja responsivo para os usuários:

  • Se seu aplicativo funcionar em segundo plano em resposta à entrada do usuário, mostre que há um progresso em andamento (por exemplo, com um ProgressBar na IU).
  • Para jogos especificamente, faça cálculos de movimentos em um thread de trabalho.
  • Se seu aplicativo tiver uma fase de configuração inicial demorada, exiba uma tela de apresentação ou renderize a visualização principal o mais rapidamente possível, indique que o carregamento está em andamento e preencha as informações de forma assíncrona. Em ambos os casos, indique de alguma forma que existe progresso em andamento, para que o usuário perceba que o aplicativo está congelado.
  • Use ferramentas de desempenho, como Systrace e Traceview,, para determinar os gargalos na capacidade de resposta do seu app.