Leider passiert es schon mal von Zeit zu Zeit, dass eine App mit der Fehlermeldung „App wird wiederholt beendet“ abstürzt und man keine Möglichkeit hat, das Problem genauer zu analysieren. Eine einfache Lösung dafür ist z.B. Firebase Crashlytics die aber gerade bei Enterprise-Apps nicht immer die beste Wahl ist.
Eine weitere Möglichkeit, mehr Details zu dem Absturz zu erhalten ist z.B. eine „Remote Logcat Session“ zu starten. Aber das ist nicht so ganz komfortabel und lohnt meiner Meinung nach nur, wenn das Kind schon den Brunnen gefallen ist und die App schon auf den Geräten verteilt ist.
Noch viel besser ist es, den Absturz von horneherein zu verhindern und in der App auf das Problem zu reagieren.
Warum wird die App denn wiederholt beendet?
Die Meldung „App wird wiederholt beendet“ wird vom Android System angezeigt, wenn eine Exception geworfen wird, die nicht in einem catch Block abgefangen wird. Z.B. wenn eine Activity, wie in dem folgenden Beispiel, eine NullPointerException wirft.
public class TestExceptionActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_exception);
throw new NullPointerException();
}
}
Normalerweise passiert das immer genau da, wo man es nicht erwartet und die Exception nicht mit einem try-catch Block behandelt wird. Das soll aber nicht bedeuten, dass man nun immer und überall einen try-catch Block verwenden soll!
Die beste Lösung ist es, alles perfekt zu testen und solche Probleme auszuschließen. Aber laut Murphys-Law “Anything that can go wrong will go wrong.” ist das leider nicht möglich :-(. Doch zum Glück gibt es eine universelle Lösung, die von der Java-VM bereitgestellt.
Die Lösung für das Problem
In der Java VM gibt es in der Thread Klasse die statische Methode setDefaultUncaughtExceptionHandler. Mit der Methode legt man den Standard-Handler fest, der aufgerufen wird, wenn ein Thread aufgrund einer nicht abgefangenen Ausnahme abrupt beendet wird und kein anderer Handler für diesen Thread definiert wurde. Als Parameter erwartet die Methode ein Objekt einer Klasse, dass folgerndes Interface implementiert.
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
Folgendes passiert innerhalb der Java VM im Falle einer nicht abgefangenen Exception:
Wenn ein Thread aufgrund einer nicht abgefangenen Exception vor der Beendigung steht, fragt die Java VM den Thread nach seinem UncaughtExceptionHandler und ruft die uncaughtException Methode des Handlers auf, wobei der Thread und die Ausnahme als Argumente übergeben werden.
In der Methode kann man nun auf jede unerwartete Exception innerhalb der App reagieren – aber wie?
Das Problem ist, dass eine Exception normalerweise ausgelöst wird, weil etwas in der App nicht so funktioniert hat wie erwartet und eine Reaktion, die das Problem behebt und dann einfach weiter macht ist in den meisten Fällen nicht möglich, daher ist meine Empfehlung, hier 2 Dinge zu tun.
- Dem Benutzer anzeigen, dass etwas Unerwartetes passiert ist. Dabei darf man ruhig seine Kreativität ausleben, etwas Humor beweisen und wenn möglich den Nutzern weiterhelfen.
Für 404 Webseiten gibt es ein Github Topic, dass man sicherlich als Quelle der Inspiration nutzen kann. >> https://github.com/topics/404-pages
Mir gefällt z.B. die folgend 404 Seite sehr gut 🙂
https://tsparticles.github.io/404-templates/space/404.html
- Den Stacktrace der Exception für eine genauere Analyse sichern. Z.B. per HTTPs auf einem Server speichern und mit den Infos das Problem analysieren und beheben.
Ein einfacher UncaughtExceptionHandler könnte z.B. wie folgt aussehen:
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "ExceptionHandler";
public static final String EXCEPTION_YOURURL = BASE_URL + "/exception.php";
private final Context mContext;
public MyUncaughtExceptionHandler (Activity activity) {
mContext = activity.getApplicationContext();
}
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
try {
Intent startAppIntent = new Intent(mContext, UncaughtExceptionActivity.class);
startAppIntent.putExtra(STACKTRACE, Log.getStackTraceString(e));
startAppIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(startAppIntent);
System.exit(1);
} catch (Exception ex) {
Log.d(TAG, Log.getStackTraceString(ex));
}
}
}
Diesen UncaughtExceptionHandler kann man dann mit folgendem Befehl überall da einfügen, wo die App abstürzen könnte. Das können Activites, Services, Broadcast Receivers, Content Provider etc. sein.
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler(this));
Ist der Handler eingetragen, wird die uncaughtException Methode aufgerufen, wenn eine nicht abgefangenen Exception geworfen wurde. Diese Methode startet dann die „UncaughtExceptionActivity“ ,übergibt als Parameter den Stacktrace und beendet die App mit System.exit(1);.
Die UncaughtExceptionActivity sollte die setDefaultUncaughtExceptionHandler Methode möglichst nicht aufrufen, da man sonst eine unschöne Endlosschliefe produzieren kann.
UncaughtExceptionActivity kann dann genutzt werden, um dem Benutzer anzeigen, dass etwas Unerwartetes passiert ist und den Stacktrace der Exception für eine genauere Analyse sichern. Das könnte dann z.B. wie folgt aussehen.
public class UncaughtExceptionActivity extends AppCompatActivity
{
private static final String TAG = "UncaughtException";
public static final String STACKTRACE = "STACKTRACE";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_uncaught_exception);
String stacktrace;
try {
stacktrace = getIntent().getExtras().getString(STACKTRACE);
} catch (Exception e){
stacktrace = Log.getStackTraceString(e);
}
reportJavaException(stacktrace);
}
public void onCloseClick(View view) {
finish();
}
private void reportJavaException(final String stacktrace) {
AsyncHttpClient client = new AsyncHttpClient();
RequestParams params = new RequestParams();
params.put("stacktrace", stacktrace);
params.put("version", BuildConfig.VERSION_NAME);
Log.d(TAG, "Post: " + EXCEPTION_YOURURL);
client.post(this, EXCEPTION_YOURURL, params,
new AsyncHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] response) {
// called when response HTTP status is "200 OK"
}
@Override
public void onFailure(int statusCode, Header[] headers, byte[] errorResponse, Throwable e) {
// called when response HTTP status is "4XX" (eg. 401, 403, 404)
}
}
);
}
}
Die Ausgestaltung des activity_uncaught_exception Layouts ist dann der Kreativität und dem persönlichen Geschmack überlassen. Für den HTTPS-Post Request zum Senden des Stacktrace nutze ich hier einfach den AsyncHttpClient der mit folgender Zeile in der build.gradle zu der App hinzugefügt werden kann.
implementation 'com.loopj.android:android-async-http:1.4.9'
Im der EXCEPTION_YOURURL Konstante muss der URL angegeben werden, der den Stacktrace auf dem Server speichert.
Als Stacktrace bezeichnet man die Ausgabe und Interpretation des Inhalts des Stacks. Ein Stacktrace wird meist zu Diagnosezwecken im Falle eines Programmabsturzes erstellt, denn damit kann die Aufrufkaskade, die zu dem Fehler führte, rekonstruiert werden.
Auf dem Stack werden u. a. die Rücksprungadressen zu den Positionen im Programm hinterlegt, von denen aus eine Prozeduren aufgerufen wurde. Es entsteht so eine Liste von Prozeduradressen, deren Rückverfolgung es erlaubt, den Pfad von Prozeduraufrufen vom Start des Programms bis hin zum aktuellen Zustand zu erkennen. Im Fehlerfall kann sich dies als hilfreich erweisen, um die aufrufende Prozedur und verwendete Übergabeparameter ausfindig zu machen.
https://de.wikipedia.org/wiki/Stacktrace
Damit der Stacktrace auf dem Server gespeichert wird, muss hier noch etwas getan werden. Ich verwende hier einfach ein kleines PHP-Script, das den HTTP-Request entgegennimmt und die Daten in eine Datei schreibt. Das Script sieht dann wie folgt aus.
<?php
$stacktrace = $_POST["stacktrace"];
if(isset($_POST["version"]) && strlen($_POST["version"])) {
$version = $_POST["version"];
} else {
$version = "NA";
}
$now = date("Y-m-d_H-i-s");
$file = "stacktrace/" . $now . "_V" . $version . ".log";
file_put_contents($file, $stacktrace);
?>
Das Unterverzeichnis „stacktrace“ muss natürlich existieren.
Bonus: Benachrichtigung bei einer Exception
Als kleinen Bonus kann man noch den folgenden Code Schnipsel an das Ende der PHP Datei anhängen. Damit wird im Falle einer Exception direkt eine Push Notification an Handy, Browser, etc. gesendet wenn man einen ntfy Server installiert hat. Natürlich kann man auch den öffentlichen https://ntfy.sh/ Dienst nutzen, sollte sich aber der Tatsache bewusst sein, dass evtl. personenbezogene Daten an einen Dritten gesendet werden könnten, was laut der DSGVO einer Zustimmung des Nutzers bedarf.
Hinweis: Bei der Nutzung von https://ntfy.sh/ kann man $login und $password und die curl option CURLOPT_USERPWD weglassen.
$login = 'LOGIN';
$password = 'PASSWORD';
$url = 'https://NTFY-SERVER/mytopic';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $stacktrace );
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$login:$password");
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Title: App Exception",
"Priority: high",
));
$result = curl_exec($ch);
curl_close($ch);
Fazit
Nun sollte die Fehlermeldung „App wird wiederholt beendet“ endgültig der Vergangenheit angehören und die Fehler, die leider immer mal wieder passieren, direkt gemeldet werden.
Mit der zusätzlichen Notification bekommt man auch zeitnah eine Info und muss nicht umständlich manuell nachsehen, ob evtl. ein Stacktrace auf dem Server gelandet ist.