Detectar e diagnosticar falhas

Um app Android falha sempre que há uma saída inesperada causada por uma exceção ou um sinal não processado. Um app que foi escrito usando Java ou Kotlin falhará se ele gerar uma exceção não processada, representada pela classe Throwable. Um app criado usando linguagens de código nativo falhará se houver um sinal não processado, como SIGSEGV, durante a execução.

Quando um app falha, o Android finaliza o processo dele e exibe uma caixa de diálogo para informar ao usuário que o app parou, conforme mostrado na Figura 1.

Falha de um app em um dispositivo Android

Figura 1. Falha de um app em um dispositivo Android

Um app não precisa estar em execução em primeiro plano para falhar. Qualquer componente dele, até mesmo broadcast receivers ou provedores de conteúdo em execução em segundo plano, pode causar uma falha no app. Essas falhas geralmente deixam os usuários confusos, porque eles não estavam interagindo ativamente com o app.

Se o app estiver apresentando falhas, use as diretrizes desta página para diagnosticar e corrigir o problema.

Detectar o problema

Você nem sempre saberá que seus usuários estão enfrentando um número excessivo de falhas no app. Se você já tiver publicado o app, o Android vitals poderá ajudar a identificar o problema.

Android vitals

O Android vitals pode ajudar a melhorar o desempenho do seu app alertando você pelo Play Console quando o app apresentar um excesso de falhas. O Android vitals considera que há um excesso de falhas quando o app:

  • exibe pelo menos uma falha em pelo menos 1,09% das sessões diárias;
  • exibe duas ou mais falhas em pelo menos 0,18% das sessões diárias.

Uma sessão diária refere-se a um dia em que seu app foi usado. Para ver informações sobre como o Google Play coleta dados do Android vitals, consulte a documentação do Play Console.

Depois de descobrir que seu app está apresentando muitas falhas, o próximo passo é diagnosticá-las.

Diagnosticar as falhas

Resolver falhas pode ser difícil. No entanto, se você conseguir identificar a causa raiz da falha, provavelmente encontrará uma solução para ela.

Há muitas situações que podem causar uma falha no app. Alguns motivos são claros, como a verificação de um valor nulo ou uma string vazia, mas outros são mais sutis, como transmitir argumentos inválidos para uma API ou até mesmo interações complexas com várias linhas de execução.

As falhas no Android produzem um stack trace, que é um snapshot da sequência de funções aninhadas chamadas no programa até o momento da falha. É possível ver stack traces de falha no Android vitals.

Ler um stack trace

O primeiro passo para corrigir uma falha é identificar o lugar em que ela acontece. Você poderá usar o stack trace disponível nos detalhes do relatório se estiver usando o Play Console ou a saída da ferramenta Logcat. Se não houver um stack trace disponível, reproduza a falha localmente testando o app de forma manual ou entrando em contato com os usuários afetados, e depois usando a ferramenta Logcat.

O trace a seguir mostra um exemplo de falha em um app criado usando a linguagem de programação Java:

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException: crash sample
at com.android.developer.crashsample.MainActivity$1.onClick(MainActivity.java:27)
at android.view.View.performClick(View.java:6134)
at android.view.View$PerformClick.run(View.java:23965)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6440)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:746)
--------- beginning of system

Um stack trace mostra duas informações essenciais para depurar uma falha:

  • O tipo de exceção gerada
  • A seção do código em que a exceção foi gerada

O tipo de exceção gerada geralmente é uma ótima pista sobre o que aconteceu de errado. Veja se houve uma IOException, um OutOfMemoryError ou outro tipo de falha e procure a documentação sobre a classe da exceção.

A classe, o método, o arquivo e o número da linha do arquivo de origem em que a exceção foi lançada são mostrados na segunda linha do stack trace. Para cada função chamada, outra linha mostra o local da chamada anterior (conhecido como frame de pilha). Subindo a pilha e examinando o código, você pode encontrar um lugar que esteja transmitindo um valor incorreto. Caso o código não apareça no stack trace, é provável que, em algum lugar, você tenha transmitido um parâmetro inválido a uma operação assíncrona. Muitas vezes, você pode descobrir o que aconteceu examinando cada linha do stack trace, localizando classes de API que você usou e confirmando se os parâmetros transmitidos estavam corretos e se você os chamou em locais permitidos.

Os stack traces de apps com código C e C++ funcionam da mesma forma.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/foo/bar:10/123.456/78910:user/release-keys'
ABI: 'arm64'
Timestamp: 2020-02-16 11:16:31+0100
pid: 8288, tid: 8288, name: com.example.testapp  >>> com.example.testapp <<<
uid: 1010332
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
    x0  0000007da81396c0  x1  0000007fc91522d4  x2  0000000000000001  x3  000000000000206e
    x4  0000007da8087000  x5  0000007fc9152310  x6  0000007d209c6c68  x7  0000007da8087000
    x8  0000000000000000  x9  0000007cba01b660  x10 0000000000430000  x11 0000007d80000000
    x12 0000000000000060  x13 0000000023fafc10  x14 0000000000000006  x15 ffffffffffffffff
    x16 0000007cba01b618  x17 0000007da44c88c0  x18 0000007da943c000  x19 0000007da8087000
    x20 0000000000000000  x21 0000007da8087000  x22 0000007fc9152540  x23 0000007d17982d6b
    x24 0000000000000004  x25 0000007da823c020  x26 0000007da80870b0  x27 0000000000000001
    x28 0000007fc91522d0  x29 0000007fc91522a0
    sp  0000007fc9152290  lr  0000007d22d4e354  pc  0000007cba01b640

backtrace:
  #00  pc 0000000000042f89  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::Crasher::crash() const)
  #01  pc 0000000000000640  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::runCrashThread())
  #02  pc 0000000000065a3b  /system/lib/libc.so (__pthread_start(void*))
  #03  pc 000000000001e4fd  /system/lib/libc.so (__start_thread)

Se você não encontrar informações no nível da classe e da função nos stack traces nativos, talvez seja necessário gerar um arquivo de símbolos de depuração nativo e fazer upload dele para o Google Play Console. Para ver mais informações, consulte Desofuscar stack traces de falhas. Para informações gerais sobre falhas nativas, consulte Diagnosticar falhas nativas.

Dicas para reproduzir uma falha

É possível que você não consiga reproduzir o problema apenas iniciando um emulador ou conectando o dispositivo ao computador. Os ambientes para desenvolvedores costumam ter mais recursos, como largura de banda, memória e armazenamento. Use o tipo de exceção para determinar qual poderia ser o recurso escasso ou para encontrar uma correlação entre a versão do Android, o tipo de dispositivo ou a versão do app.

Erros de memória

Se você encontrar um OutOfMemoryError, poderá criar um emulador com baixa capacidade de memória para realizar testes. A Figura 2 mostra as configurações do AVD Manager em que você pode controlar a quantidade de memória do dispositivo.

Configuração de memória do AVD Manager

Figura 2. Configuração de memória do AVD Manager.

Exceções de rede

Como os usuários costumam entrar e sair da cobertura da rede móvel ou do Wi-Fi, em um app as exceções de rede geralmente não podem ser tratadas como erros, mas como condições operacionais normais que ocorrem inesperadamente.

Se você precisar reproduzir uma exceção de rede, como uma UnknownHostException, ative o modo avião enquanto seu aplicativo tenta usar a rede.

Outra opção é reduzir a qualidade da rede no emulador escolhendo uma emulação de velocidade e/ou um atraso de rede. Use as configurações de Speed e Latency no AVD Manager ou inicie o emulador com as sinalizações -netdelay e -netspeed, conforme mostrado no exemplo de linha de comando a seguir:

emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm

Esse exemplo define um atraso de 20 segundos em todas as solicitações de rede e uma velocidade de upload e download de 14,4 Kbps. Para saber mais sobre as opções de linha de comando do emulador, consulte Iniciar o emulador pela linha de comando.

Ler com Logcat

Quando você realizar as etapas para reproduzir a falha, poderá usar uma ferramenta como o Logcat para ver mais informações.

A saída do logcat mostrará quais outras mensagens de registro foram gravadas, junto a outras mensagens do sistema. Não se esqueça de desativar instruções Log extras que você adicionou, porque exibi-las desperdiça CPU e bateria enquanto o app está em execução.

Evitar falhas causadas por exceções de ponteiro nulo

As exceções de ponteiro nulo, identificadas pelo tipo de erro de tempo de execução NullPointerException, ocorrem quando você está tentando acessar um objeto nulo, normalmente invocando os métodos ou acessando os membros dele. Exceções de ponteiro nulo são a principal causa de falhas de apps no Google Play. A finalidade do valor nulo é indicar que o objeto está ausente, por exemplo, ele ainda não foi criado ou atribuído. Para evitar exceções de ponteiro nulo, você precisa garantir que as referências de objeto com as quais está trabalhando não sejam nulas antes de chamar métodos ou ao tentar acessar os membros deles. Se a referência do objeto for nula, processe esse caso corretamente. Por exemplo, saia de um método antes de executar qualquer operação na referência do objeto e grave informações em um registro de depuração.

Como você não quer executar verificações de objeto nulo para cada parâmetro de cada método chamado, pode usar o ambiente de desenvolvimento integrado ou o tipo de objeto para representar a nulidade.

Linguagem de programação Java

As seções a seguir se aplicam à linguagem de programação Java.

Avisos durante a compilação

Anote os parâmetros dos métodos e retorne valores com @Nullable e @NonNull para receber avisos do ambiente de desenvolvimento integrado durante a compilação. Esses avisos informam que um objeto anulável será gerado:

Aviso de exceção de ponteiro nulo

Essas verificações de objeto nulo são usadas para objetos que você sabe que podem ser nulos. Uma exceção em um objeto @NonNull é uma indicação de um erro no seu código que precisa ser resolvido.

Erros durante a compilação

Como a nulidade precisa ser significativa, é possível incorporá-la aos tipos que você usa para que haja uma verificação de objetos nulos durante a compilação. Se você souber que um objeto pode ser nulo e que a nulidade precisa ser tratada, envolva-a em um objeto como Optional. Sempre dê preferência a tipos que aceitam a nulidade.

Kotlin

Em Kotlin, a nulidade (link em inglês) faz parte do sistema de tipos. Por exemplo, uma variável precisa ser declarada desde o início como anulável ou não anulável. Os tipos anuláveis são marcados com ?:

// non-null
var s: String = "Hello"

// null
var s: String? = "Hello"

As variáveis não anuláveis não podem receber um valor nulo, e é preciso verificar a nulidade das variáveis anuláveis antes de usá-las como não nulas.

Se você não quiser verificar explicitamente se um objeto é nulo, use o operador de chamada segura ?.:

val length: Int? = string?.length  // length is a nullable int
                                   // if string is null, then length is null

Como prática recomendada, verifique o caso nulo de um objeto anulável, ou seu app poderá entrar em estados inesperados. Se o aplicativo não falhar mais com uma NullPointerException, você não saberá se esses erros existem.

Veja a seguir algumas maneiras de verificar se há objetos nulos:

  • Verificações if

    val length = if(string != null) string.length else 0
    

    Devido à transmissão inteligente e à verificação de objetos nulos, o compilador do Kotlin sabe que o valor da string não é nulo. Assim, você pode usar a referência diretamente, sem a necessidade de usar o operador de chamada segura.

  • Operador Elvis ?: (link em inglês)

    Esse operador permite que você declare o seguinte: "se o objeto não for nulo, retorne-o; caso contrário, retorne outro item".

    val length = string?.length ?: 0
    

Você ainda poderá gerar uma NullPointerException em Kotlin. Veja a seguir as situações mais comuns em que isso acontece:

  • Ao gerar uma NullPointerException explicitamente.
  • Quando você está usando o operador de declaração nula !!. Esse operador converte qualquer valor para um tipo não nulo, gerando uma NullPointerException se o valor for nulo.
  • Ao acessar uma referência nula de um tipo de plataforma.

Tipos de plataforma

Os tipos de plataforma são declarações de objetos do Java. Esses tipos são tratados de forma específica (link em inglês). As verificações de objetos nulos não são aplicadas, portanto, a garantia de não haver nulidade é igual à usada em Java. Quando você acessa uma referência de tipo de plataforma, o Kotlin não cria erros durante a compilação, mas essas referências podem causar erros no tempo de execução. Veja o exemplo a seguir na documentação do Kotlin:

val list = ArrayList<String>() // non-null (constructor result) list.add("Item")
val size = list.size // non-null (primitive int) val item = list[0] // platform
type inferred (ordinary Java object) item.substring(1) // allowed, may throw an
                                                       // exception if item == null

O Kotlin usa a inferência de tipo quando um valor de plataforma é atribuído a uma variável do Kotlin, ou você pode definir o tipo esperado. A melhor maneira de garantir o estado de nulidade correto de uma referência do Java é usar anotações de nulidade (por exemplo, @Nullable) no código Java. O compilador do Kotlin representará essas referências como tipos anuláveis ou não anuláveis, não como tipos de plataforma.

As APIs Java Jetpack são anotadas com @Nullable ou @NonNull conforme o necessário, e uma abordagem semelhante foi adotada no SDK do Android 11. Os tipos desse SDK, que são usados no Kotlin, serão representados como tipos anuláveis ou não anuláveis corretos.

Devido ao sistema de tipos do Kotlin, os apps apresentaram uma grande redução em falhas de NullPointerException. Por exemplo, o app Google Home apresentou uma redução de 30% nas falhas causadas por exceções de ponteiro nulo durante o ano em que o desenvolvimento de novos recursos foi migrado para o Kotlin.