Aplikacja na Androida ulega awarii, gdy nieoczekiwane wyjście jest spowodowane nieobsługiwanym wyjątkiem lub sygnałem. Aplikacja napisana w Javie lub Kotlin ulega awarii, jeśli zgłasza nieobsługiwany wyjątek reprezentowany przez klasę Throwable
. Aplikacja napisana przy użyciu kodu maszynowego lub w przypadku języka C++ ulega awarii, jeśli podczas działania wystąpi nieobsługiwany sygnał, np. SIGSEGV
.
Gdy aplikacja ulegnie awarii, Android zakończy jej proces i wyświetli okno z informacją, że aplikacja została zatrzymana, jak widać na ilustracji 1.
Aby aplikacja mogła ulec awarii, nie musi być uruchomiona na pierwszym planie. Awarię aplikacji może powodować każdy składnik aplikacji, nawet komponenty takie jak odbiorniki czy dostawcy treści, które działają w tle. Te awarie są często mylące, ponieważ użytkownicy nie aktywnie korzystają z aplikacji.
Jeśli aplikacja ulega awarii, skorzystaj ze wskazówek na tej stronie, aby zdiagnozować i rozwiązać problem.
Wykryj problem
Nie zawsze wiesz, że u użytkowników Twojej aplikacji występują awarie. Jeśli aplikacja została już opublikowana, możesz sprawdzić jej częstotliwość w Android Vitals.
Android Vitals
Android Vitals może pomóc Ci monitorować i poprawiać częstotliwość awarii aplikacji. Android Vitals mierzy kilka częstotliwości awarii:
- Częstotliwość awarii: odsetek aktywnych użytkowników dziennie, u których wystąpiła awaria.
Częstotliwość awarii widocznych dla użytkowników: odsetek aktywnych użytkowników dziennie, u których wystąpiła co najmniej 1 awaria podczas aktywnego korzystania z Twojej aplikacji. Aplikacja jest uznawana za używaną, jeśli prezentuje jakakolwiek aktywność lub korzysta z usługi na pierwszym planie.
Częstotliwość wielu awarii: odsetek aktywnych użytkowników dziennie, u których wystąpiły co najmniej 2 awarie.
Aktywny użytkownik dziennie to unikalny użytkownik, który korzysta z Twojej aplikacji w ciągu 1 dnia na 1 urządzeniu, a potem w ramach wielu sesji. Jeśli użytkownik korzysta z aplikacji na więcej niż 1 urządzeniu w ciągu 1 dnia, każde z tych urządzeń będzie miało wpływ na liczbę aktywnych użytkowników w danym dniu. Jeśli wielu użytkowników korzysta z tego samego urządzenia w ciągu 1 dnia, jest to liczone jako 1 aktywny użytkownik.
Częstotliwość awarii widocznych dla użytkowników jest podstawowym wskaźnikiem, co oznacza, że wpływa na możliwość odkrycia Twojej aplikacji w Google Play. Jest ważne, ponieważ zliczane awarie występują zawsze, gdy użytkownik korzysta z aplikacji, co powoduje największe zakłócenia.
W przypadku tych danych Google Play ma 2 progi niewłaściwego działania:
- Ogólny próg niewłaściwego działania: widoczna awaria wystąpiła u co najmniej 1, 09% aktywnych użytkowników dziennie na wszystkich modelach urządzeń.
- Próg niewłaściwego działania na urządzenie: widoczna awaria wystąpiła u co najmniej 8% aktywnych użytkowników dziennie na 1 modelu urządzenia.
Jeśli Twoja aplikacja przekracza ogólny próg niewłaściwego działania, prawdopodobnie będzie trudniejsza do odkrycia na wszystkich urządzeniach. Jeśli aplikacja przekracza próg niewłaściwego działania na niektórych urządzeniach, może być na nich trudniejsza do odkrycia, a na jej stronie w Sklepie może wyświetlać się ostrzeżenie.
Android Vitals może powiadamiać Cię w Konsoli Play o nadmiernej liczbie awarii aplikacji.
Informacje o tym, jak Google Play gromadzi dane Android Vitals, znajdziesz w dokumentacji Konsoli Play.
Diagnozowanie awarii
Gdy już wykryjesz, że aplikacja zgłasza awarie, następnym krokiem jest ich zdiagnozowanie. Naprawianie awarii może być trudne. Jeśli jednak poznasz główną przyczynę awarii, najprawdopodobniej uzyskasz rozwiązanie problemu.
Awarię aplikacji może wystąpić w wielu sytuacjach. Niektóre przyczyny są oczywiste, np. sprawdzanie wartości null lub pustego ciągu znaków. Inne są jednak bardziej subtelne, np. przekazywane nieprawidłowe argumenty do interfejsu API, a nawet złożone interakcje wielowątkowe.
Awarie na Androidzie powodują utworzenie zrzutu stosu, czyli zrzutu sekwencji zagnieżdżonych funkcji wywołanych w programie do momentu awarii. Zrzuty stosu awarii możesz wyświetlać w Android Vitals.
Jak odczytywać zrzut stosu
Pierwszym krokiem do naprawy awarii jest identyfikacja miejsca, w którym do niej dochodzi. Jeśli korzystasz z Konsoli Play lub danych wyjściowych narzędzia logcat, możesz użyć zrzutu stosu dostępnego w szczegółach raportu. Jeśli nie masz dostępnego zrzutu stosu, możesz odtworzyć awarię lokalnie, testując aplikację ręcznie lub kontaktując się z użytkownikami, których dotyczy problem, i odtwórz ją przy użyciu narzędzia Logcat.
Poniższy ślad pokazuje przykład awarii aplikacji napisanej w języku programowania 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
Zrzut stosu zawiera 2 informacje, które mają kluczowe znaczenie przy debugowaniu awarii:
- Rodzaj zgłoszonego wyjątku.
- Sekcja kodu, w której jest zgłaszany wyjątek.
Rodzaj zgłoszonego wyjątku jest zwykle bardzo wyraźną wskazówką, co poszło nie tak. Sprawdź, czy jest to IOException
, OutOfMemoryError
czy coś innego, i znajdź dokumentację na temat klasy wyjątku.
Klasa, metoda, plik i numer wiersza pliku źródłowego, w którym został zgłoszony wyjątek, są wymienione w drugim wierszu zrzutu stosu. Dla każdej wywołanej funkcji kolejny wiersz przedstawia poprzednią witrynę wywołania (tzw. ramkę stosu). Przeglądając kod, możesz zauważyć miejsce, które przekazuje nieprawidłową wartość. Jeśli Twój kod nie pojawia się w zrzucie stosu, prawdopodobnie gdzieś w operacji asynchronicznej został przekazany nieprawidłowy parametr. Często możesz dowiedzieć się, co się stało, przeglądając każdy wiersz zrzutu stosu, znajdując wszelkie użyte klasy interfejsu API i potwierdzając, że przekazane parametry są prawidłowe oraz że funkcja została wywołana z dozwolonego miejsca.
Śledzenie stosu aplikacji z kodem w językach C i C++ działa w ten sam sposób.
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
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)
Jeśli w natywnych zrzutach stosu nie widzisz informacji na poziomie klasy i funkcji, konieczne może być wygenerowanie natywnego pliku z symbolami debugowania i przesłanie go do Konsoli Google Play. Więcej informacji znajdziesz w artykule o usuwaniu zaciemnienia zrzutów stosu awarii. Ogólne informacje o awariach natywnych znajdziesz w artykule Diagnozowanie awarii natywnych.
Wskazówki dotyczące odtwarzania awarii
Możliwe, że nie uda Ci się odtworzyć problemu, uruchamiając emulator lub podłączając urządzenie do komputera. Środowiska programistyczne zazwyczaj mają więcej zasobów, takich jak przepustowość, pamięć i miejsce na dane. Użyj typu wyjątku, aby określić, którego zasobu brakuje, lub znaleźć korelację między wersją Androida, typem urządzenia lub wersją aplikacji.
Błędy pamięci
Jeśli masz OutOfMemoryError
, możesz utworzyć emulator o małej ilości pamięci do testowania. Ilustracja
2 przedstawia ustawienia menedżera AVD, za pomocą których można kontrolować ilość pamięci
urządzenia.
Wyjątki sieci
Użytkownicy często przemieszczają się poza zasięg sieci komórkowej lub Wi-Fi, dlatego wyjątki w sieci aplikacji zwykle nie powinny być traktowane jako błędy, ale jako normalne warunki działania, które mogą wystąpić nieoczekiwanie.
Jeśli chcesz odtworzyć wyjątek sieci, np. UnknownHostException
, spróbuj włączyć tryb samolotowy, gdy aplikacja próbuje korzystać z sieci.
Innym sposobem jest obniżenie jakości sieci w emulatorze przez wybranie emulacji szybkości sieci lub opóźnienia sieci. Możesz użyć ustawień Szybkość i Czas oczekiwania w menedżerze AVD lub uruchomić emulator od flag -netdelay
i -netspeed
, jak widać w tym przykładzie wiersza poleceń:
emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm
W tym przykładzie ustawiamy 20-sekundowe opóźnienie dla wszystkich żądań sieciowych oraz prędkość przesyłania i pobierania 14,4 kb/s. Więcej informacji o opcjach wiersza poleceń emulatora znajdziesz w artykule o uruchamianiu emulatora z poziomu wiersza poleceń.
Czytanie za pomocą narzędzia LogCat
Gdy uda Ci się wykonać kroki potrzebne do odtworzenia awarii, możesz użyć narzędzia takiego jak logcat
, aby uzyskać więcej informacji.
Wyniki logcat pokażą, jakie inne komunikaty logu zostały wyświetlone, a także inne komunikaty z systemu. Nie zapomnij wyłączyć wszystkich dodanych instrukcji Log
, ponieważ ich drukowanie zwiększa wykorzystanie procesora i baterii podczas działania aplikacji.
Zapobiegaj awariom spowodowanym przez wyjątki wskaźnika null
Wyjątki dla wskaźników null (identyfikowane przez typ błędu środowiska wykonawczego NullPointerException
) występują, gdy próbujesz uzyskać dostęp do obiektu, który ma wartość null. Zwykle odbywa się to przez wywołanie jego metod lub uzyskanie dostępu do jego elementów. Wyjątki dotyczące wskaźnika null to najczęstsza przyczyna awarii aplikacji w Google Play. Wartość null wskazuje, że obiektu brakuje, np. nie został on jeszcze utworzony ani przypisany. Aby uniknąć wyjątków dotyczących wskaźników o wartości null, upewnij się, że odwołania do obiektów, z którymi pracujesz, nie mają wartości null, zanim wywołasz ich metody lub spróbujesz uzyskać dostęp do ich elementów. Jeśli odwołanie do obiektu ma wartość null, postępuj zgodnie ze wskazówkami (np. wyjdź z metody przed wykonaniem jakichkolwiek operacji na tym obiekcie i zapisz informacje w dzienniku debugowania).
Ponieważ nie chcesz mieć kontroli o wartości null dla każdego wywoływanego parametru każdej metody, możesz polegać na IDE lub typie obiektu do sygnalizowania wartości null.
Język programowania Java
Poniższe sekcje dotyczą języka programowania Java.
Ostrzeżenia dotyczące czasu kompilacji
Aby otrzymywać z IDE ostrzeżenia o czasie kompilacji, dodaj adnotacje do parametrów i zwracanych wartości za pomocą @Nullable
i @NonNull
. Te ostrzeżenia sugerują, że należy spodziewać się obiektu z wartością null:
Te testy null dotyczą obiektów, o których wiesz, że mogą mieć wartość null. Wyjątek dotyczący obiektu @NonNull
wskazuje na błąd w kodzie, który należy naprawić.
Błędy czasowe kompilacji
Ponieważ dopuszczalność wartości null powinna mieć znaczenie, możesz umieścić ją w używanych typach, aby umożliwić sprawdzanie czasu kompilowania wartości null. Jeśli wiesz, że obiekt może być wartością null i obsługiwać wartość null, możesz go spakować w obiekt taki jak Optional
.
Zawsze wybieraj typy, które przekazują wartość null.
Kotlin
W kotlinach dopuszczalność wartości null jest częścią systemu typów. Na przykład zmienna musi być zadeklarowana od początku jako dopuszczająca lub niezawierająca wartości null. Typy z możliwością wartości null są oznaczone znakiem ?
:
// non-null
var s: String = "Hello"
// null
var s: String? = "Hello"
Zmiennym niedopuszczającym wartości null nie można przypisać wartości null, a zmienne z możliwością wartości null trzeba sprawdzić, zanim zostaną użyte jako zmienne.
Jeśli nie chcesz sprawdzać wartości null bezpośrednio, możesz użyć bezpiecznego operatora wywołań ?.
:
val length: Int? = string?.length // length is a nullable int
// if string is null, then length is null
Sprawdzoną metodą jest rozwiązanie problemu z wartością null w przypadku obiektu z możliwością null. W przeciwnym razie aplikacja może znaleźć się w nieoczekiwanym stanie. Jeśli aplikacja nie ulega już awarii z NullPointerException
, nie dowiesz się, że te błędy występują.
Oto kilka sposobów na sprawdzenie wartości null:
if
kontrolival length = if(string != null) string.length else 0
Dzięki Smart-cast i sprawdzaniu wartości null kompilator Kotlin wie, że wartość ciągu nie ma wartości null, dzięki czemu można używać odwołania bezpośrednio, bez konieczności stosowania operatora bezpiecznego wywołania.
-
Ten operator pozwala określić, że „jeśli obiekt nie ma wartości null, zwracaj obiekt, w przeciwnym razie zwracaj coś innego”.
val length = string?.length ?: 0
Nadal możesz otrzymać NullPointerException
w Kotlin. Oto najczęstsze sytuacje:
- Gdy uruchamiasz jawny rzut
NullPointerException
. - Gdy używasz operatora asercji null
!!
. Ten operator konwertuje dowolną wartość na typ inny niż null, powodując zgłoszenieNullPointerException
, jeśli wartość jest równa null. - Podczas uzyskiwania dostępu do odwołania o wartości null typu platformy.
Typy platform
Typy platform to deklaracje obiektów pochodzące z Javy. Te typy są specjalnie traktowane. Testy o wartości null nie są egzekwowane, więc gwarancja nienull jest taka sama jak w Java. Gdy otworzysz odniesienie do typu platformy, Kotlin nie tworzy błędów podczas kompilacji, ale te odwołania mogą powodować błędy w czasie działania. Zobacz ten przykład z dokumentacji 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 polega na założeniu, że wartość platformy jest przypisana do zmiennej Kotlin. Możesz też określić typ spodziewany. Najlepszym sposobem na zapewnienie poprawnego stanu wartości null dla odwołania z języka Java jest użycie w kodzie Java adnotacji z wartością null (np. @Nullable
). Kompilator Kotlin przedstawia te odwołania jako rzeczywiste lub niewartościowe typy platform, a nie typy platform.
W razie potrzeby interfejsy API Java Jetpack zostały oznaczone adnotacją @Nullable
lub @NonNull
. Podobne podejście zastosowano w pakiecie SDK do Androida 11.
Typy pochodzące z tego pakietu SDK używane w usłudze Kotlin będą reprezentowane jako poprawne typy z wartością null lub bez wartości null.
Ze względu na system typów stosowanych przez Kotlin zaobserwowaliśmy znaczny spadek liczby NullPointerException
awarii w aplikacjach. W roku, w którym aplikacja Google Home przeniosła tworzenie nowych funkcji do Kotlin, liczba awarii w aplikacji Google Home spadła o 30%.