Сбои

Приложение для Android аварийно завершает работу при неожиданном завершении, вызванном необработанным исключением или сигналом. Приложение, написанное на Java или Kotlin, аварийно завершает работу, если оно генерирует необработанное исключение, представленное классом Throwable . Приложение, написанное на машинном коде или C++, аварийно завершает работу, если во время его выполнения возникает необработанный сигнал, например SIGSEGV .

При сбое приложения Android завершает процесс приложения и отображает диалоговое окно, сообщающее пользователю о том, что приложение остановлено, как показано на рисунке 1.

Сбой приложения на устройстве под управлением Android.
Рисунок 1. Сбой приложения на устройстве под управлением Android.

Для сбоя приложения не обязательно, чтобы оно работало в фоновом режиме. Любой компонент приложения, даже такие компоненты, как широковещательные приемники или поставщики контента, работающие в фоновом режиме, могут привести к сбою приложения. Эти сбои часто сбивают пользователей с толку, поскольку они не взаимодействовали активно с вашим приложением.

Если ваше приложение вылетает, вы можете воспользоваться инструкциями на этой странице, чтобы диагностировать и устранить проблему.

Проведите диагностику сбоев.

После того, как вы определили, что ваше приложение сообщает о сбоях, следующим шагом будет их диагностика. Устранение сбоев может быть сложной задачей. Однако, если вы сможете определить первопричину сбоя, скорее всего, вы сможете найти решение.

Существует множество ситуаций, которые могут привести к сбою в вашем приложении. Некоторые причины очевидны, например, проверка на нулевое значение или пустую строку, но другие более сложны, например, передача недопустимых аргументов в API или даже сложные многопоточные взаимодействия.

При сбоях в Android создается трассировка стека, которая представляет собой снимок последовательности вложенных функций, вызываемых в вашей программе до момента сбоя. Вы можете просмотреть трассировку стека при сбоях в Android Vitals .

Как читать трассировку стека

The first step to fix a crash is to identify the place where it happens. You can use the stack trace available in the report details if you are using Play Console or the output of the logcat tool. If you don't have a stack trace available, you should locally reproduce the crash, either by manually testing the app or by reaching out to affected users, and reproduce it while using logcat.

Приведенный ниже пример трассировки показывает сбой в приложении, написанном с использованием Jetpack Compose:

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException
    at com.android.developer.crashsample.ComposableSingletons$MainActivityKt.lambda$0(MainActivity.kt:27)
    at androidx.compose.foundation.ClickableNode.handleUpEvent(Clickable.kt:958)
    at androidx.compose.foundation.ClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:895)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:446)
    at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:181)
    at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:118)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:2650)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2969)
    at android.app.Activity.dispatchTouchEvent(Activity.java:4683)
    at android.os.Looper.loop(Looper.java:398)
    at android.app.ActivityThread.main(ActivityThread.java:9569)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:918)

Трассировка стека отображает два важных для отладки сбоя фрагмента информации:

  • Тип выброшенного исключения.
  • Участок кода, где возникает исключение.

Тип выброшенного исключения обычно очень точно указывает на причину проблемы. Проверьте, является ли это IOException , OutOfMemoryError или что-то другое, и найдите документацию по классу исключения.

На второй строке трассировки стека указаны класс, метод, файл и номер строки исходного файла, в котором возникло исключение. Для каждой вызванной функции отдельная строка показывает место предыдущего вызова (так называемый кадр стека).

By walking up the stack and examining the code, you may find a place that is passing an incorrect value. If your code doesn't appear in the stack trace, it is likely that somewhere, you passed an invalid parameter into an asynchronous operation. You can often figure out what happened by examining each line of the stack trace, finding any API classes that you used, and confirming that the parameters you passed were correct, and that you called it from a place that is allowed.

Трассировка стека для приложений, использующих код на C и C++, работает практически одинаково.

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

Если в трассировках стека нативных приложений вы не видите информации на уровне классов и функций, вам может потребоваться сгенерировать файл отладочных символов нативного приложения и загрузить его в консоль Google Play. Дополнительную информацию см. в разделе «Деобфускация трассировок стека сбоев». Общую информацию о сбоях нативных приложений см. в разделе «Диагностика сбоев нативных приложений» .

Советы по воспроизведению сбоя

It's possible that you can't quite reproduce the problem just by starting an emulator or connecting your device to your computer. Development environments tend to have more resources, such as bandwidth, memory, and storage. Use the type of exception to determine what could be the resource that is scarce, or find a correlation between the version of Android, device type or your app's version.

Ошибки памяти

Если возникает ошибка OutOfMemoryError , вы можете создать эмулятор с небольшим объемом памяти для тестирования. На рисунке 2 показаны настройки диспетчера AVD, где вы можете управлять объемом памяти на устройстве.

Настройка памяти в диспетчере AVD
Рисунок 2. Настройки памяти в диспетчере AVD.

Сетевые исключения

Поскольку пользователи часто перемещаются между зонами покрытия мобильной или Wi-Fi сети, в приложении сетевые исключения обычно не следует рассматривать как ошибки , а скорее как нормальные условия работы, возникающие неожиданно.

Если вам необходимо воспроизвести сетевое исключение, например UnknownHostException , попробуйте включить режим полета, пока ваше приложение пытается использовать сеть.

Другой вариант — снизить качество сети в эмуляторе, выбрав эмуляцию скорости сети, задержки сети или и то, и другое. Вы можете использовать настройки скорости и задержки в диспетчере AVD или запустить эмулятор с флагами -netdelay и -netspeed , как показано в следующем примере командной строки:

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

В этом примере задается задержка в 20 секунд для всех сетевых запросов и скорость загрузки и выгрузки 14,4 Кбит/с. Для получения дополнительной информации о параметрах командной строки эмулятора см. раздел «Запуск эмулятора из командной строки» .

Чтение с помощью logcat

Как только вам удастся воспроизвести сбой, вы можете использовать такой инструмент, как logcat , чтобы получить более подробную информацию.

Вывод команды logcat покажет вам, какие ещё сообщения журнала вывели, а также другие сообщения из системы. Не забудьте отключить все лишние сообщения Log , которые вы добавили, поскольку их вывод расходует ресурсы процессора и заряд батареи во время работы вашего приложения.

Предотвращение сбоев, вызванных исключениями нулевого указателя.

Null pointer exceptions (identified by the runtime error type NullPointerException ) occur when you're trying to access an object that is null, typically by invoking its methods or accessing its members. Null pointer exceptions are the largest cause of app crashes on Google Play. The purpose of null is to signify that the object is missing - for example, it hasn't been created or assigned yet.

Чтобы избежать исключений NullPointerException, необходимо убедиться, что ссылки на объекты, с которыми вы работаете, не равны нулю, прежде чем вызывать методы или пытаться получить доступ к их членам. Если ссылка на объект равна нулю, обработайте этот случай должным образом (например, выйдите из метода до выполнения каких-либо операций с ссылкой на объект и запишите информацию в отладочный журнал).

Поскольку вам не нужны проверки на null для каждого параметра каждого вызываемого метода, вы можете полагаться на IDE или на тип объекта, который будет указывать на возможность присвоения значения null.

Котлин

В Kotlin возможность присвоения значения null является частью системы типов. Например, переменная должна быть объявлена ​​с самого начала как допускающая или не допускающая значение null. Допускающие значение null типы помечаются знаком ? :

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

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

Непустые переменные нельзя присвоить значение NULL, а пустые переменные необходимо проверять на возможность присвоения значения NULL, прежде чем использовать их в качестве пустых.

Если вы не хотите явно проверять наличие значения null, вы можете использовать оператор безопасного вызова ?.

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

В качестве лучшей практики убедитесь, что вы обрабатываете случай нулевого значения для объектов, допускающих значение NULL, иначе ваше приложение может оказаться в непредвиденных ситуациях. Если ваше приложение больше не будет вылетать с NullPointerException , вы даже не будете знать о существовании этих ошибок.

Ниже приведены несколько способов проверки на наличие значения null:

  • if проверяет

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

    Благодаря функции интеллектуального приведения типов и проверке на null, компилятор Kotlin знает, что строковое значение не равно null, поэтому он позволяет использовать ссылку напрямую, без необходимости использования оператора безопасного вызова.

  • ?: Оператор Элвиса

    Этот оператор позволяет указать: «Если объект не равен null, вернуть объект; в противном случае вернуть что-то другое».

    val length = string?.length ?: 0
    

В Kotlin по-прежнему может возникнуть ошибка NullPointerException . Ниже перечислены наиболее распространенные ситуации:

  • Когда вы явно генерируете исключение NullPointerException .
  • При использовании оператора проверки на null !! этот оператор преобразует любое значение в ненулевой тип, выбрасывая NullPointerException если значение равно null.
  • При обращении к нулевой ссылке платформенного типа.

Типы платформ

Типы платформ — это объявления объектов, заимствованные из Java. Эти типы обрабатываются особым образом ; проверки на null не так строго соблюдаются, поэтому гарантия отсутствия null такая же, как в Java. При обращении к ссылке на тип платформы Kotlin не вызывает ошибок компиляции, но такие ссылки могут привести к ошибкам во время выполнения. См. следующий пример из документации 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 relies on type inference when a platform value is assigned to a Kotlin variable, or you can define what type to expect. The best way to ensure the correct nullability state of a reference coming from Java is to use nullability annotations (for example, @Nullable ) in your Java code. The Kotlin compiler will represent these references as actual nullable or non-nullable types, not as platform types.

API Java Jetpack аннотированы с помощью @Nullable или @NonNull по мере необходимости, и аналогичный подход используется в SDK Android 11. Типы из этого SDK, используемые в Kotlin, будут представлены как корректные типы, допускающие или не допускающие значение null.

Система типов Kotlin значительно снижает количество сбоев, NullPointerException . Например, в приложении Google Home количество сбоев, вызванных исключениями NullPointerException, сократилось на 30% за год, в течение которого разработка новых функций была переведена на Kotlin.