Choques

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 arroja una excepción no controlada, representada por la clase Throwable. Una app escrita con código máquina o C++ falla si, durante su ejecución, se produce 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 fallas cuando usan tu app. Si ya la publicaste, puedes usar Android vitals para consultar la tasa de fallas de tu app.

Android vitals

Android vitals puede ayudarte a supervisar y mejorar la tasa de fallas de tu app. Android vitals mide varias tasas de fallas:

  • Tasa de fallas: Es el porcentaje de usuarios activos por día que experimentaron algún tipo de falla.
  • Tasa de fallas percibidas por el usuario: Es el porcentaje de usuarios activos por día que experimentaron al menos una falla mientras usaban tu app de forma activa (una falla percibida por el usuario). Se considera que una app está en uso activo si muestra actividad o ejecuta cualquier servicio en primer plano.

  • Tasa de fallas múltiples: Es el porcentaje de usuarios activos por día que experimentaron al menos dos fallas.

Un usuario activo por día es un usuario único que utiliza tu app durante un solo día en un solo dispositivo, posiblemente en varias sesiones. Si un usuario usa tu app en más de un dispositivo durante un día específico, se calcula cada dispositivo como parte de la cantidad de usuarios activos de ese día. Si varios usuarios usan el mismo dispositivo en un día determinado, todos ellos cuentan como un usuario activo.

La tasa de fallas percibidas por el usuario es una métrica esencial, lo que significa que afecta la visibilidad de tu app en Google Play. Es importante porque solo cuenta las fallas que ocurren cuando el usuario interactúa con la app y, por lo tanto, causan la mayor cantidad de interrupciones.

Play definió dos umbrales de comportamiento inadecuado en esta métrica:

  • Umbral general de comportamiento inadecuado: Al menos el 1.09% de los usuarios activos por día experimentan una falla percibida por el usuario en todos los modelos de dispositivos.
  • Umbral de comportamiento inadecuado por dispositivo: Al menos el 8% de los usuarios activos por día experimentan una falla percibida por el usuario en un solo modelo de dispositivo.

Si tu app supera el umbral general de comportamiento inadecuado, es probable que sea menos visible en todos los dispositivos. Si la app supera el umbral de comportamiento inadecuado en algunos dispositivos, es probable que sea menos visible en estos y que se muestre una advertencia en la ficha de Play Store.

Android vitals puede enviarte alertas a través de Play Console cuando tu app presenta una cantidad excesiva de fallas.

Si deseas obtener información sobre cómo Google Play recopila datos de Android vitals, consulta la documentación de Play Console.

Cómo diagnosticar las fallas

Una vez que hayas identificado que tu app informa fallas, el siguiente paso es diagnosticarlas. 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 cadena 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 arrojó
  • La sección del código en la que se arroja la excepción

Por lo general, el tipo de excepción que se arroja es un indicio claro de lo que salió mal. Observa si se trata de una 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 se arrojó 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 aplicación, 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 la app.

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

Las excepciones de puntero nulo (que se identifican con 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 para 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 maneras 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 cadena no es nulo, 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 APIs de Java Jetpack se anotaron con @Nullable o @NonNull según fue 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.