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).
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.
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:
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
checksval 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.
-
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 gibtNullPointerException
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.