Awarie

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.

Awaria aplikacji na urządzeniu z Androidem

Rysunek 1. Awaria aplikacji na urządzeniu z Androidem

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.

Ustawienie pamięci w menedżerze AVD

Rysunek 2. Ustawienie pamięci w menedżerze AVD

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:

Ostrzeżenie o wyjątku wskaźnika o 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 kontroli

    val 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.

  • ?: Operator Elvis

    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łoszenie NullPointerException, 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%.