Сбои

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

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

Сбой приложения на устройстве Android

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

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

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

Обнаружить проблему

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

Android Vitals

Android Vitals может помочь вам отслеживать и снижать частоту сбоев вашего приложения. Android Vitals измеряет несколько показателей сбоев:

  • Частота сбоев: процент ваших ежедневных активных пользователей, у которых произошел сбой любого типа.
  • Частота сбоев, воспринимаемых пользователями: процент активных пользователей в день, у которых произошел хотя бы один сбой во время активного использования вашего приложения (сбой, воспринимаемый пользователями). Приложение считается активно используемым, если оно отображает какую-либо активность или выполняет какую-либо службу переднего плана .

  • Частота множественных сбоев: процент ваших ежедневных активных пользователей, у которых произошло как минимум два сбоя.

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

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

Компания Play определила два порога плохого поведения по этому показателю:

  • Общий порог плохого поведения: по крайней мере 1,09% ежедневных активных пользователей сталкиваются с сбоем, по мнению пользователей, на всех моделях устройств.
  • Порог плохого поведения для каждого устройства: по крайней мере 8 % ежедневных активных пользователей сталкиваются с сбоем, по мнению пользователей, для одной модели устройства .

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

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

Информацию о том, как Google Play собирает данные Android Vitals, можно найти в документации Play Console .

Диагностика сбоев

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

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

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

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

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

Следующая трассировка показывает пример сбоя приложения, написанного с использованием языка программирования 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

Трассировка стека показывает две части информации, которые имеют решающее значение для отладки сбоя:

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

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

Класс, метод, файл и номер строки исходного файла, в котором создается исключение, отображаются во второй строке трассировки стека. Для каждой вызванной функции в другой строке отображается место предыдущего вызова (называемое кадром стека). Поднявшись по стеку и изучив код, вы можете обнаружить место, в котором передается неверное значение. Если ваш код не отображается в трассировке стека, вполне вероятно, что где-то вы передали недопустимый параметр в асинхронную операцию. Часто вы можете выяснить, что произошло, изучив каждую строку трассировки стека, найдя все используемые вами классы API и подтвердив, что переданные вами параметры были правильными и что вы вызвали его из разрешенного места.

Трассировки стека для приложений с кодом 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. Дополнительные сведения см. в разделе Трассировки стека деобфускации . Общую информацию о собственных сбоях см. в разделе Диагностика собственных сбоев .

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

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

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

Если у вас есть 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 , поскольку их печать расходует ресурсы процессора и батареи во время работы вашего приложения.

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

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

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

язык программирования Java

Следующие разделы относятся к языку программирования Java.

Предупреждения о времени компиляции

Аннотируйте параметры своих методов и возвращаемые значения с помощью @Nullable и @NonNull чтобы получать предупреждения во время компиляции от IDE. Эти предупреждения побуждают вас ожидать объект, допускающий значение NULL:

Предупреждение об исключении нулевого указателя

Эти проверки на нулевое значение предназначены для объектов, которые, как вы знаете, могут иметь значение нулевое. Исключение для объекта @NonNull указывает на ошибку в вашем коде, которую необходимо устранить.

Ошибки времени компиляции

Поскольку возможность обнуления должна иметь смысл, вы можете встроить ее в используемые типы, чтобы во время компиляции выполнялась проверка на наличие значения null. Если вы знаете, что объект может иметь значение NULL и что возможность обнуления должна быть обработана, вы можете обернуть его в объект типа Optional . Всегда следует отдавать предпочтение типам, которые передают возможность обнуления.

Котлин

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

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

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

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

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

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

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

Ниже приведены некоторые способы проверки нуля:

  • if проверяет

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

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

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

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

    val length = string?.length ?: 0
    

Вы все равно можете получить NullPointerException в Котлине. Ниже приведены наиболее распространенные ситуации:

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

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

Типы платформы — это объявления объектов, исходящие из Java. Эти типы подвергаются специальной обработке ; проверки на 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 полагается на вывод типа, когда значение платформы присваивается переменной Kotlin, или вы можете определить, какой тип ожидать. Лучший способ обеспечить правильное состояние нулевого значения ссылки, поступающей из Java, — использовать аннотации нулевого значения (например, @Nullable ) в вашем Java-коде. Компилятор Kotlin будет представлять эти ссылки как фактические типы, допускающие или не допускающие значения NULL, а не как типы платформы.

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

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