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 que ainda assim pareça lento, trave ou congele por períodos significativos ou demore 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) na linha de execução de UI, de modo que o sistema não consegue 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 na linha de execução de UI. É 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 na linha de execução de UI. Em vez disso, crie uma linha de execução de trabalho e realize a maioria das operações nela. Isso mantém a linha de execução de UI (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 essas linhas de execução geralmente são feitas 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 pelo 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 uma única linha de execução, a "linha de execução de UI" ou a "linha de execução principal". Isso significa que qualquer ação do seu aplicativo na linha de execução de UI 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 na linha de execução de IU deve fazer o mínimo possível de trabalho nessa linha. 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 na linha de execução de um worker (ou, no caso de operações de bancos de dados, por meio de uma solicitação assíncrona).

A maneira mais eficaz de criar uma linha de execução de trabalho para operações mais longas é usar a classe AsyncTask. Basta estender AsyncTask e implementar o método doInBackground() para realizar o trabalho. Para postar mudanças de progresso para o usuário, você pode chamar publishProgress(), que invoca o método de callback onProgressUpdate(). Na sua implementação de onProgressUpdate() (que é executada na linha de execução 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 essa linha de execução 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 da linha de execução como "segundo plano" chamando Process.setThreadPriority() e passando THREAD_PRIORITY_BACKGROUND. Se você não definir a linha de execução como uma prioridade mais baixa, o app ainda poderá ficar lento, porque ele opera, por padrão, na mesma prioridade da linha de execução de IU.

Se você implementar Thread ou HandlerThread, verifique se a linha de execução de IU não é bloqueada enquanto aguarda a conclusão da linha de execução de trabalho. Não chame Thread.wait() nem Thread.sleep(). Em vez de ser bloqueada enquanto aguarda a conclusão de uma linha de execução de trabalho, a linha de execução principal deve fornecer um Handler a ser postado novamente pelas outras linhas após a conclusão. Projetar seu aplicativo dessa maneira permitirá que a linha de execução de IU dele permaneça responsiva à 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 na linha de execução 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 linhas de execução de trabalho, seu aplicativo deve 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 receptores de transmissão sob demanda.

Dica: você pode usar StrictMode para ajudar a localizar operações possivelmente longas, como as de rede ou de banco de dados, que podem estar sendo realizadas por engano na linha de execução 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 está executando algo em segundo plano em resposta a uma entrada do usuário, use uma ProgressBar para mostrar o progresso da execução na IU.
  • Para jogos especificamente, faça cálculos de movimentos em uma linha de execução 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.