Abstürze

Eine Android-App stürzt immer dann ab, wenn durch eine unbehandelte Ausnahme oder ein unbehandeltes Signal ein unerwartetes Beenden auftritt. Eine mit Java oder Kotlin geschriebene App stürzt ab, wenn sie eine unbehandelte Ausnahme auslöst, die durch die Klasse Throwable dargestellt wird. Eine Anwendung, die mit Maschinencode oder C++ geschrieben wurde, stürzt ab, wenn während der Ausführung ein unbehandeltes Signal wie SIGSEGV auftritt.

Wenn eine App abstürzt, beendet Android den Prozess der App und zeigt einen Dialog an, in dem der Nutzer darüber informiert wird, dass die App beendet wurde (siehe Abbildung 1).

App-Absturz auf einem Android-Gerät

Abbildung 1: App-Absturz auf einem Android-Gerät

Eine App muss nicht im Vordergrund ausgeführt werden, damit sie abstürzt. Jede App-Komponente, selbst Komponenten wie Übertragungsempfänger oder Contentanbieter, die im Hintergrund ausgeführt werden, können zum Absturz einer App führen. Diese Abstürze sind für Nutzer oft verwirrend, da sie nicht aktiv mit Ihrer App interagiert haben.

Wenn bei Ihrer App Abstürze auftreten, können Sie mithilfe der Anleitungen auf dieser Seite das Problem diagnostizieren und beheben.

Problem erkennen

Vielleicht weißt du nicht immer, dass es bei deinen Nutzern zu Abstürzen kommt, wenn sie deine App verwenden. Wenn du deine App bereits veröffentlicht hast, kannst du dir mit Android Vitals die Absturzraten für deine App anzeigen lassen.

Android Vitals

Mit Android Vitals kannst du die Absturzrate deiner App beobachten und verbessern. In Android Vitals werden verschiedene Absturzraten gemessen:

  • Absturzrate:Prozentsatz der aktiven Nutzer pro Tag, bei denen eine Absturzart aufgetreten ist.
  • Vom Nutzer wahrgenommene Absturzrate:Prozentsatz der aktiven Nutzer pro Tag, bei denen während der aktiven Nutzung Ihrer App mindestens ein Absturz aufgetreten ist (ein vom Nutzer wahrgenommener Absturz). Eine App gilt als aktiv, wenn sie Aktivitäten anzeigt oder Dienste im Vordergrund ausführt.

  • Mehrfachabsturzrate:Prozentsatz der aktiven Nutzer pro Tag, bei denen mindestens zwei Abstürze aufgetreten sind.

Ein täglich aktiver Nutzer ist ein einzelner Nutzer, der Ihre App an einem Tag auf einem einzelnen Gerät und möglicherweise über mehrere Sitzungen hinweg verwendet. Wenn ein Nutzer Ihre App an einem Tag auf mehr als einem Gerät verwendet, wird jedes Gerät zur Anzahl der aktiven Nutzer für diesen Tag addiert. Wenn mehrere Nutzer an einem Tag dasselbe Gerät verwenden, wird dies als ein aktiver Nutzer gezählt.

Die vom Nutzer wahrgenommene Absturzrate ist ein Vitalparameter, d. h., er beeinflusst die Sichtbarkeit Ihrer App bei Google Play. Dies ist wichtig, da die gezählten Abstürze immer auftreten, wenn der Nutzer mit der Anwendung interagiert, und die größte Störung verursachen.

Google Play hat für diesen Messwert zwei Grenzwerte zu unerwünschtem Verhalten festgelegt:

  • Grenzwert zu unerwünschtem Verhalten:Bei mindestens 1, 09% der aktiven Nutzer pro Tag tritt auf allen Gerätemodellen ein vom Nutzer wahrgenommener Absturz auf.
  • Grenzwert zu unerwünschtem Verhalten auf einzelnen Geräten:Bei mindestens 8% der aktiven Nutzer pro Tag tritt bei einem einzelnen Gerätemodell ein vom Nutzer wahrgenommener Absturz auf.

Wenn deine App den allgemeinen Grenzwert zu unerwünschtem Verhalten überschreitet, ist sie wahrscheinlich auf allen Geräten weniger gut sichtbar. Wenn deine App auf einigen Geräten den Grenzwert für unerwünschtes Verhalten pro Gerät überschreitet, ist sie auf diesen Geräten wahrscheinlich weniger gut sichtbar und in deinem Store-Eintrag wird möglicherweise eine Warnung angezeigt.

Android Vitals kann dich über die Play Console benachrichtigen, wenn deine App übermäßige Abstürze aufweist.

Informationen dazu, wie Google Play Android Vitals-Daten erhebt, findest du in der Play Console-Dokumentation.

Abstürze diagnostizieren

Sobald Sie festgestellt haben, dass Ihre App Abstürze meldet, besteht der nächste Schritt darin, diese zu diagnostizieren. Es kann schwierig sein, Abstürze zu beheben. Wenn Sie jedoch die Ursache des Absturzes finden können, ist es höchstwahrscheinlich auch eine Lösung.

Es gibt viele Situationen, die einen Absturz Ihrer Anwendung verursachen können. Einige Gründe sind offensichtlich, z. B. die Prüfung auf einen Nullwert oder einen leeren String, andere sind jedoch subtiler, z. B. das Übergeben ungültiger Argumente an eine API oder sogar komplexe Multithread-Interaktionen.

Abstürze auf Android erzeugen einen Stacktrace. Dieser ist ein Snapshot der Abfolge verschachtelter Funktionen, die in Ihrem Programm bis zum Absturz aufgerufen werden. Sie können Absturz-Stacktraces in Android Vitals ansehen.

Stacktrace lesen

Der erste Schritt zur Behebung eines Absturzes besteht darin, den Ort zu ermitteln, an dem er auftritt. Sie können den in den Berichtsdetails verfügbaren Stacktrace verwenden, wenn Sie die Play Console oder die Ausgabe des logcat-Tools nutzen. Wenn kein Stacktrace verfügbar ist, sollten Sie den Absturz lokal reproduzieren. Dazu können Sie entweder die Anwendung manuell testen oder die betroffenen Nutzer kontaktieren und den Absturz mithilfe von Logcat reproduzieren.

Der folgende Trace zeigt ein Beispiel für einen Absturz einer Anwendung, die in der Programmiersprache Java geschrieben wurde:

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

Ein Stacktrace enthält zwei Informationen, die für das Debugging eines Absturzes entscheidend sind:

  • Der Typ der ausgelösten Ausnahme.
  • Der Codeabschnitt, in dem die Ausnahme ausgelöst wird.

Die Art der ausgelösten Ausnahme ist normalerweise ein sehr starker Hinweis darauf, was schiefgelaufen ist. Sehen Sie sich an, ob es sich um IOException, OutOfMemoryError oder etwas anderes handelt, und lesen Sie die Dokumentation zur Ausnahmeklasse.

Die Klasse, Methode, Datei und Zeilennummer der Quelldatei, in der die Ausnahme ausgelöst wird, werden in der zweiten Zeile eines Stacktrace angezeigt. Für jede aufgerufene Funktion wird in einer weiteren Zeile die vorherige Aufrufseite (als Stackframe bezeichnet) angezeigt. Wenn Sie den Stack hinaufgehen und den Code untersuchen, finden Sie möglicherweise Stellen, an denen ein falscher Wert übergeben wird. Wenn Ihr Code nicht im Stacktrace angezeigt wird, haben Sie wahrscheinlich irgendwo einen ungültigen Parameter an einen asynchronen Vorgang übergeben. Sie können häufig herausfinden, was passiert ist, indem Sie jede Zeile des Stacktrace untersuchen, alle verwendeten API-Klassen ermitteln und prüfen, ob die übergebenen Parameter korrekt waren und dass Sie sie von einer zulässigen Stelle aus aufgerufen haben.

Stacktraces für Apps mit C- und C++-Code funktionieren ähnlich.

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

Wenn in nativen Stacktraces keine Informationen auf Klassen- und Funktionsebene angezeigt werden, müssen Sie möglicherweise eine Symboldatei zum Debuggen von nativem Code generieren und in die Google Play Console hochladen. Weitere Informationen finden Sie unter Offenlegung von Absturz-Stacktraces. Allgemeine Informationen zu nativen Abstürzen finden Sie unter Native Abstürze diagnostizieren.

Tipps zum Reproduzieren eines Absturzes

Möglicherweise können Sie das Problem nicht reproduzieren, indem Sie einfach einen Emulator starten oder Ihr Gerät mit Ihrem Computer verbinden. Entwicklungsumgebungen haben in der Regel mehr Ressourcen, z. B. Bandbreite, Arbeitsspeicher und Speicher. Ermitteln Sie anhand des Ausnahmetyps, welche Ressource knapp ist, oder stellen Sie einen Zusammenhang zwischen der Android-Version, dem Gerätetyp oder der Version Ihrer App her.

Speicherfehler

Wenn Sie einen OutOfMemoryError haben, könnten Sie einen Emulator mit geringer Arbeitsspeicherkapazität zum Testen erstellen. Abbildung 2 zeigt die Einstellungen des AVD-Managers, mit denen Sie den Speicherplatz auf dem Gerät steuern können.

Speichereinstellung im AVD-Manager

Abbildung 2: Speichereinstellung im AVD-Manager

Netzwerkausnahmen

Da Nutzer häufig die Mobilfunk- oder WLAN-Abdeckung verlassen und wieder entfernen, sollten Ausnahmen in Anwendungsnetzwerken in der Regel nicht als Fehler behandelt werden, sondern als normale Betriebsbedingungen, die unerwartet auftreten.

Wenn Sie eine Netzwerkausnahme (z. B. UnknownHostException) reproduzieren müssen, aktivieren Sie den Flugmodus, während Ihre Anwendung versucht, das Netzwerk zu verwenden.

Eine weitere Option besteht darin, die Qualität des Netzwerks im Emulator zu verringern, indem Sie eine Netzwerkgeschwindigkeitsemulation und/oder eine Netzwerkverzögerung auswählen. Sie können die Einstellungen für Geschwindigkeit und Latenz im AVD-Manager verwenden oder den Emulator mit den Flags -netdelay und -netspeed starten, wie im folgenden Befehlszeilenbeispiel gezeigt:

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

In diesem Beispiel wird bei allen Netzwerkanfragen eine Verzögerung von 20 Sekunden und eine Upload- und Downloadgeschwindigkeit von 14,4 Kbit/s festgelegt. Weitere Informationen zu Befehlszeilenoptionen für den Emulator finden Sie unter Emulator über die Befehlszeile starten.

Lesen mit Logcat

Sobald Sie die Schritte zum Reproduzieren des Absturzes ausgeführt haben, können Sie ein Tool wie logcat verwenden, um weitere Informationen zu erhalten.

Die Logcat-Ausgabe zeigt Ihnen, welche anderen Lognachrichten Sie gedruckt haben, sowie andere Logmeldungen aus dem System. Vergessen Sie nicht, alle zusätzlichen Log-Anweisungen zu deaktivieren, die Sie hinzugefügt haben, da beim Drucken CPU und Akku verbraucht werden, während die Anwendung ausgeführt wird.

Abstürze aufgrund von Null-Pointer-Ausnahmen vermeiden

Null-Pointer-Ausnahmen (durch den Laufzeitfehlertyp NullPointerException identifiziert) treten auf, wenn Sie versuchen, auf ein Objekt zuzugreifen, das null ist, in der Regel durch Aufrufen seiner Methoden oder durch Zugriff auf seine Mitglieder. Nullzeiger-Ausnahmen sind die größte Ursache für App-Abstürze bei Google Play. Null zeigt an, dass das Objekt fehlt, z. B., wenn es noch nicht erstellt oder zugewiesen wurde. Um Nullzeigerausnahmen zu vermeiden, müssen Sie dafür sorgen, dass die Objektverweise, mit denen Sie arbeiten, nicht null sind, bevor Sie Methoden für sie aufrufen oder versuchen, auf ihre Mitglieder zuzugreifen. Wenn der Objektverweis null ist, muss die Groß-/Kleinschreibung gut gehandhabt werden. Beenden Sie beispielsweise eine Methode, bevor Sie Vorgänge für den Objektverweis ausführen, und schreiben Sie Informationen in ein Fehlerbehebungsprotokoll.

Da nicht für jeden Parameter jeder aufgerufenen Methode Null-Prüfungen erforderlich sein sollen, können Sie sich auf die IDE oder den Objekttyp verlassen, um die Null-Zulässigkeit anzugeben.

Programmiersprache Java

Die folgenden Abschnitte beziehen sich auf die Programmiersprache Java.

Warnungen für Kompilierungszeit

Annotieren Sie die Parameter und Rückgabewerte Ihrer Methoden mit @Nullable und @NonNull, um von der IDE Warnungen zur Kompilierungszeit zu erhalten. Diese Warnungen fordern Sie auf, ein Objekt zu erwarten, für das Nullwerte zulässig sind:

Warnung zur Null-Pointer-Ausnahme

Diese Nullprüfungen beziehen sich auf Objekte, von denen Sie wissen, dass sie null sein könnten. Eine Ausnahme für ein @NonNull-Objekt ist ein Hinweis auf einen Fehler in Ihrem Code, der behoben werden muss.

Kompilierungszeitfehler

Da die Null-Zulässigkeit aussagekräftig sein sollte, können Sie sie in die von Ihnen verwendeten Typen einbetten, sodass eine Compile-Zeitprüfung auf Null durchgeführt wird. Wenn Sie wissen, dass ein Objekt null sein kann und diese Null-Zulässigkeit behandelt werden soll, können Sie es in ein Objekt wie Optional einbinden. Sie sollten immer Typen bevorzugen, die Null-Zulässigkeit vermitteln.

Kotlin

In Kotlin ist Null-Zulässigkeit Teil des Typsystems. Beispielsweise muss eine Variable von Anfang an als Nullwerte zulässig oder nicht zulässig sein. Typen, für die Nullwerte zulässig sind, sind mit einem ? gekennzeichnet:

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

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

Variablen, die keine Nullwerte zulassen, darf kein Nullwert zugewiesen werden. Variablen, die keine Nullwerte zulassen, müssen auf Null-Zulässigkeit geprüft werden, bevor sie als Nicht-Null-Werte verwendet werden können.

Wenn Sie nicht explizit nach NULL suchen möchten, können Sie den Operator ?. für den sicheren Aufruf verwenden:

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

Als Best Practice sollten Sie den Null-Fall für ein Objekt, in dem Nullwerte zulässig sind, berücksichtigen. Andernfalls könnte Ihre Anwendung in einen unerwarteten Status geraten. Wenn Ihre Anwendung mit NullPointerException nicht mehr abstürzt, wissen Sie nicht, dass diese Fehler vorliegen.

So können Sie auf NULL-Werte prüfen:

  • if checks

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

    Aufgrund von Smartcast und Nullprüfung weiß der Kotlin-Compiler, dass der Stringwert nicht null ist, sodass Sie die Referenz direkt verwenden können, ohne dass der Operator für den sicheren Aufruf erforderlich ist.

  • ?: Elvis-Operator

    Mit diesem Operator können Sie festlegen, dass das Objekt zurückgegeben wird, wenn das Objekt nicht null ist. Andernfalls wird etwas anderes zurückgegeben.

    val length = string?.length ?: 0
    

Du kannst NullPointerException weiterhin in Kotlin erhalten. Dies sind die häufigsten Situationen:

  • Wenn Sie explizit ein NullPointerException auslösen.
  • Wenn Sie den !!-Operator „null Assertion“ verwenden. Dieser Operator wandelt jeden Wert in einen Nicht-Null-Typ um und gibt NullPointerException aus, wenn der Wert null ist.
  • Beim Zugriff auf eine Nullreferenz eines Plattformtyps.

Plattformtypen

Plattformtypen sind Objektdeklarationen aus Java. Diese Typen werden speziell behandelt. Null-Prüfungen werden nicht so erzwungen, dass die Nicht-Null-Garantie mit der in Java identisch ist. Wenn Sie auf eine Plattformtypreferenz zugreifen, verursacht Kotlin keine Fehler bei der Kompilierungszeit. Diese Verweise können jedoch zu Laufzeitfehlern führen. Das folgende Beispiel stammt aus der Kotlin-Dokumentation:

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 stützt sich auf die Typinferenz, wenn einer Kotlin-Variable ein Plattformwert zugewiesen wird. Alternativ lässt sich der zu erwartende Typ definieren. Die beste Möglichkeit, den korrekten Status der Null-Zulässigkeit einer Referenz aus Java sicherzustellen, besteht darin, in Ihrem Java-Code Annotationen für die Null-Zulässigkeit zu verwenden (z. B. @Nullable). Der Kotlin-Compiler stellt diese Verweise als Typen dar, die keine Nullwerte enthalten dürfen, und nicht als Plattformtypen.

Java Jetpack APIs wurden bei Bedarf mit @Nullable oder @NonNull annotiert. Im Android 11 SDK wird ein ähnlicher Ansatz verfolgt. Von diesem SDK stammende Typen, die in Kotlin verwendet werden, werden als korrekte Typen mit Null-Zulässigkeit oder ohne Null-Zulässigkeit dargestellt.

Durch das Typsystem von Kotlin konnten wir feststellen, dass bei Apps weniger NullPointerException-Abstürze auftreten. Beispielsweise verzeichnete die Google Home App im Jahr, in dem die Entwicklung neuer Funktionen zu Kotlin migriert wurde, eine Reduzierung der Abstürze um 30 %, die durch Nullzeiger-Ausnahmen verursacht wurden.