Приложение Android аварийно завершает работу всякий раз, когда происходит неожиданный выход из-за необработанного исключения или сигнала. Приложение, написанное с использованием Java или Kotlin, аварийно завершает работу, если оно генерирует необработанное исключение, представленное классом Throwable
. Приложение, написанное с использованием машинного кода или C++, аварийно завершает работу, если во время его выполнения возникает необработанный сигнал, например SIGSEGV
.
При сбое приложения Android завершает процесс приложения и отображает диалоговое окно, сообщающее пользователю, что приложение остановлено, как показано на рисунке 1.
Приложению не обязательно быть запущенным на переднем плане, чтобы оно вышло из строя. Любой компонент приложения, даже такие компоненты, как приемники вещания или поставщики контента, работающие в фоновом режиме, могут привести к сбою приложения. Эти сбои часто сбивают с толку пользователей, поскольку они не активно взаимодействовали с вашим приложением.
Если в вашем приложении происходят сбои, вы можете использовать рекомендации на этой странице для диагностики и устранения проблемы.
Обнаружить проблему
Вы не всегда можете знать, что у ваших пользователей возникают сбои при использовании вашего приложения. Если вы уже опубликовали свое приложение, вы можете использовать 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-менеджера, в которых вы можете контролировать объем памяти на устройстве.
Сетевые исключения
Поскольку пользователи часто входят и выходят из зоны покрытия мобильной сети или сети 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 знает, что строковое значение не равно нулю, поэтому позволяет использовать ссылку напрямую, без необходимости использования оператора безопасного вызова.
Этот оператор позволяет вам указать: «если объект не равен NULL, верните объект; в противном случае верните что-то еще».
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.