Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Cómo detectar y diagnosticar fallas

Una app para Android falla cada vez que se produce una salida inesperada a causa de una señal o excepción no controlada. Una app escrita con Java o Kotlin falla si muestra una excepción no controlada, representada por la clase Throwable. Una app escrita con lenguajes de código nativo falla si se produce, durante su ejecución, una señal no controlada, como SIGSEGV.

Cuando falla una app, Android termina el proceso de la app y muestra un diálogo para avisarle al usuario que esta se detuvo, como se muestra en la figura 1.

Falla de una app en un dispositivo Android

Figura 1: Falla de una app en un dispositivo Android

No es necesario que una app se ejecute en primer plano para que falle. Cualquier componente, incluidos los receptores de emisión o proveedores de contenido que se ejecutan en segundo plano, puede provocar que una app falle. A menudo, estas fallas confunden a los usuarios porque no estaban interactuando de manera activa con la app.

Si tu app tiene fallas, puedes seguir las indicaciones de esta página para diagnosticar el problema y corregirlo.

Cómo detectar el problema

Quizás no siempre estés al tanto de que tus usuarios están experimentando una cantidad excesiva de fallas con tu app. Si ya la publicaste, Android vitals puede ayudarte detectar el problema.

Android vitals

Android vitals puede ayudarte a mejorar el rendimiento de tu app. Para ello, te envía alertas a través de Play Console cuando tu app presenta una cantidad excesiva de fallas. Android vitals considera que una app falla de manera excesiva en los siguientes casos:

  • Cuando falla una vez, como mínimo, en al menos el 1.09% de sus sesiones diarias.
  • Cuando falla dos o más veces en al menos el 0.18% de sus sesiones diarias.

Una sesión diaria corresponde a un día en el que se usó tu app. Para descubrir cómo Google Play recopila datos de Android vitals, consulta la documentación de Play Console.

Una vez que se te informa que tu app falla con demasiada frecuencia, el próximo paso consiste en diagnosticar fallas.

Cómo diagnosticar fallas

Resolver fallas puede ser difícil. Sin embargo, si logras identificar la causa raíz, lo más probable es que puedas encontrar una solución.

Hay muchas razones por las que puede fallar tu app. Algunas son obvias, como comprobar si hay un valor nulo o una string vacía, pero otras son más sutiles, como pasar argumentos no válidos a una API o interacciones complejas con varios subprocesos.

En Android, las fallas producen un seguimiento de pila (una instantánea de la secuencia de funciones anidadas que se llamaron en el programa hasta el momento en que se produjo la falla). Puedes consultar los seguimientos de pila de fallas en Android vitals.

Cómo leer un seguimiento de pila

El primer paso para corregir una falla consiste en identificar el lugar en el que sucede. Puedes utilizar el seguimiento de pila disponible en los detalles del informe si usas Play Console o el resultado de la herramienta logcat. Si no tienes un seguimiento de pila disponible, debes reproducir la falla a nivel local, ya sea probando la app de forma manual o comunicándote con los usuarios afectados, y reproduciendo la falla mientras usas la herramienta logcat.

En el seguimiento que aparece a continuación, se muestra un ejemplo de una falla en una app escrita con el lenguaje de programación 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

En un seguimiento de pila, se muestran dos datos que son fundamentales para depurar una falla:

  • El tipo de excepción que se mostró
  • La sección del código en la que ocurre la excepción

Por lo general, el tipo de excepción que se muestra es un indicio claro de lo que salió mal. Observa si se trata de un IOException, un OutOfMemoryError o algún otro error, y busca la documentación sobre la clase de excepción.

La clase, el método, el archivo y el número de línea del archivo de origen en el que ocurrió la excepción se muestran en la segunda línea del seguimiento de pila. Por cada función a la que se llamó, otra línea muestra el sitio de llamada anterior (conocido como marco de pila). Si subes la pila y examinas el código, es posible que encuentres un lugar en el que se pasó un valor incorrecto. Si no se muestra tu código en el seguimiento de pila, es probable que, en algún lugar, hayas pasado un parámetro no válido a una operación asíncrona. A menudo, puedes averiguar qué sucedió examinando cada línea del seguimiento de pila, buscando las clases de API que usaste y confirmando si los parámetros que pasaste eran correctos y si el lugar desde el que realizaste la llamada estaba permitido.

Los seguimientos de pila para las apps con código C y C++ funcionan de la misma manera.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
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)

Si no ves la información de clase y función en los seguimientos de pila nativos, puede que debas generar un archivo de símbolos de depuración nativo y subirlo a Google Play Console. Para obtener más información, consulta Cómo desofuscar el seguimiento de pila de las fallas. Para obtener información general sobre las fallas por errores en código nativo, consulta Cómo diagnosticar fallas por errores en código nativo.

Sugerencias para reproducir una falla

Posiblemente no puedas reproducir el problema iniciando un emulador o conectando el dispositivo a la computadora. Los entornos de desarrollo tienden a contar con más recursos, como ancho de banda, memoria y almacenamiento. Usa el tipo de excepción para determinar cuál podría ser el recurso escaso o encuentra una correlación entre la versión de Android, el tipo de dispositivo o la versión de tu app.

Errores de memoria

Si experimentas un OutOfMemoryError, puedes probarlo creando un emulador con capacidad de memoria baja. En la figura 2, se muestra la configuración del Administrador de AVD que te permite controlar la cantidad de memoria en el dispositivo.

Configuración de memoria en el Administrador de AVD

Figura 2: Configuración de memoria en el Administrador de AVD

Excepciones de red

Debido a que los usuarios tienen y dejan de tener cobertura de red móvil o Wi-Fi con frecuencia, en una app, las excepciones de red no se deberían tratar como errores, sino como condiciones de operación normales que suceden de forma inesperada.

Si necesitas reproducir una excepción de red, como UnknownHostException, activa el modo de avión mientras la aplicación intenta usar la red.

Otra opción consiste en reducir la calidad de la red en el emulador. Para ello, elige una emulación de velocidad o un retraso de la red. Puedes usar las opciones de configuración de Velocidad y Latencia en el Administrador de AVD, o bien iniciar el emulador con las marcas -netdelay y -netspeed, como se muestra en el siguiente ejemplo de la línea de comandos:

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

En este ejemplo, se define un retraso de 20 segundos en todas las solicitudes de red y una velocidad de subida y descarga de 14.4 kbps. Si deseas obtener más información sobre las opciones de la línea de comandos para el emulador, consulta Cómo iniciar el emulador desde la línea de comandos.

Cómo leer con logcat

Una vez que conoces los pasos para reproducir la falla, puedes usar una herramienta como logcat si deseas obtener más información.

El resultado de logcat te mostrará otros mensajes de registro que imprimiste, junto con información adicional del sistema. No olvides desactivar las declaraciones Log adicionales que agregaste, ya que imprimirlas consume recursos de la CPU y la batería mientras se ejecuta tu app.

Cómo evitar las fallas que causan las excepciones de puntero nulo

Las excepciones de puntero nulo (que identifica el tipo de error de tiempo de ejecución NullPointerException) se producen cuando intentas acceder a un objeto nulo. Para ello, por lo general, invocas a sus métodos o accedes a sus miembros. Las excepciones de puntero nulo son la causa principal de las fallas de apps en Google Play. El propósito del puntero nulo es indicar que falta el objeto, por ejemplo, que todavía no se creó ni se asignó. Para evitar excepciones de puntero nulo, debes asegurarte de que las referencias a objetos con las que trabajas no sean nulas antes de llamar a los métodos en ellos o de intentar acceder a sus miembros. Si la referencia del objeto es nula, controla bien este caso (por ejemplo, sal de un método antes de realizar cualquier operación en la referencia del objeto y escribe la información en un registro de depuración).

Como no deseas tener verificaciones nulas para cada parámetro de cada método llamado, puedes usar el IDE o el tipo de objeto a fin de indicar la nulabilidad.

Lenguaje de programación Java

Las siguientes secciones se aplican al lenguaje de programación Java.

Advertencias sobre el tiempo de compilación

Anota los parámetros de tus métodos y muestra valores con @Nullable y @NonNull para recibir advertencias sobre el tiempo de compilación del IDE. Estas advertencias te solicitan que esperes un objeto anulable:

Advertencia sobre la excepción del puntero nulo

Estas verificaciones nulas son para objetos que sabes que podrían ser nulos. Una excepción en un objeto @NonNull es una indicación de un error en tu código que se debe abordar.

Errores en el tiempo de compilación

Como la nulabilidad debe ser significativa, puedes incorporarla en los tipos que usas para que haya una verificación del tiempo de compilación para la nulabilidad. Si sabes que un objeto puede ser nulo y que la nulabilidad debe controlarse, puedes unirla a un objeto como Optional. Siempre debes optar por los tipos que transmiten nulabilidad.

Kotlin

En Kotlin, la nulabilidad es parte del sistema de tipos. Por ejemplo, una variable debe declararse desde el comienzo como anulable o no anulable. Los tipos anulables se marcan con ?:

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

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

A las variables no anulables no se les puede asignar un valor nulo, y debe verificarse la nulabilidad de las variables anulables antes de utilizarlas como no nulas.

Si no quieres verificar la nulabilidad de forma explícita, puedes usar el operador de llamada segura ?.:

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

Como práctica recomendada, asegúrate de abordar el caso nulo de un objeto anulable, o tu app podría recibir estados inesperados. Si tu aplicación ya no falla más con NullPointerException, no sabrás que estos errores existen.

A continuación, se incluyen algunas manera de verificar la nulabilidad:

  • Verificaciones if

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

    Debido a la emisión inteligente y la verificación nula, el compilador de Kotlin sabe que el valor de la string no es nula, por lo que te permite usar la referencia de forma directa, sin la necesidad del operador de llamada segura.

  • ?: Operador Elvis

    Este operador te permite indicar "si el objeto no es nulo, mostrar el objeto; de lo contrario, mostrar algo más".

    val length = string?.length ?: 0
    

Todavía puedes obtener un elemento NullPointerException en Kotlin. Las siguientes son las situaciones más comunes:

  • Cuando arrojas NullPointerException de manera explícita.
  • Cuando usas el operador !! de aserción nula. Este operador convierte cualquier valor en un tipo no nulo y arroja NullPointerException si el valor es nulo.
  • Cuando se accede a una referencia nula de un tipo de plataforma.

Tipos de plataformas

Los tipos de plataforma son declaraciones de objeto que provienen de Java. Estos tipos se tratan de manera especial; las verificaciones nulas no se aplican, por lo que la garantía no nula es la misma que en Java. Cuando accedes a una referencia del tipo de plataforma, Kotlin no genera errores del tiempo de compilación, pero estas referencias pueden provocar errores del tiempo de ejecución. Consulta el siguiente ejemplo de la documentación de 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

Kotlin se basa en la inferencia de tipo cuando se asigna un valor de plataforma a una variable de Kotlin, o puedes definir qué tipo esperar. La mejor manera de garantizar el estado de nulabilidad correcto de una referencia que proviene de Java es usar anotaciones de nulabilidad (por ejemplo, @Nullable) en tu código Java. El compilador de Kotlin representará estas referencias como tipos anulables o no anulables en lugar de como tipos de plataformas.

Las API de Java Jetpack se anotaron con @Nullable o @NonNull según sea necesario, y se adoptó un enfoque similar en el SDK de Android 11. Los tipos que provienen de este SDK, que se usan en Kotlin, se representarán como tipos anulables o no anulables.

Debido al sistema de tipos de Kotlin, notamos que las apps tienen una reducción importante en las fallas de NullPointerException. Por ejemplo, la app de Google Home experimentó una reducción del 30% en las fallas que causan excepciones de puntero nulo durante el año en que migró el desarrollo de características nuevas a Kotlin.