Android-Entwickler ohne AsyncTask-Klasse 24.11.2019, 17:21 Uhr

Adios AsyncTask

Google hat die altbekannte AsyncTask-Klasse als deprecated markiert.
Was dies für Android-Entwickler bedeutet, was Google motiviert und wie man nun weiter vorgehen sollte, beleuchtet dieser Artikel.
Das Problem ist so alt wie die GUI-Entwicklung: sobald ein Betriebssystem mehrere Threads unterstützt, kann man mit den GUI-Steuerelementen nur aus dem GUI-Thread heraus interagieren. Ursache dafür ist, dass man den GUI-Stack in diesem Fall nicht threadsicher auslegen muss.
Was auf den ersten Blick nach Faulheit des Betriebssystemanbieters klingt, hat einen handfesten Grund. Man soll nämlich häufige Aufgaben so schnell wie möglich abarbeiten, auch dann, wenn exotischere Aufgaben beim Entwickler zusätzliche Arbeit verursachen.

Interaktion mit Steuerelementen

Müsste man jede Interaktion mit Steuerelementen threadsicher implementieren, so wäre jeder normale Zugriff mit zusätzlichem Aufwand belastet. Die unterm Strich geringe Reduktion des Entwickleraufwands verlangsamt das System immens – in den meisten Fällen ein unguter Tausch.
Blickt man seit Anfang November auf Reddit oder einen anderen Platz, auf dem sich Android-Entwickler gerne aufhalten, so findet man Hunderte Verweise auf das unter https://android-review.googlesource.com/c/platform/frameworks/base/+/1156409/6/core/java/android/os/AsyncTask.java bereitstehende und in Bild 1 gezeigte Commit.
Die AsyncTask-Klasse wurde von Google als deprecated markiert (Bild 1)
Quelle: Hanna
Wer die Abbildung genauer betrachtet, findet folgende Erklärung für die Abkündigung: »AsyncTask was intended to enable proper and easy use of the UI thread. However, the most common use case was for integrating into UI, and that would cause Context leaks, missed callbacks, or crashes on configuration changes. It also has inconsistent behavior on different versions of the platform, swallows exceptions from {@code doInBackground}, and does not providemuch utility over using {@link Executor}s directly.«
Google geht davon aus, dass die fälschliche Verwendung der AsyncTask-Klasse für diverse Probleme im Framework verantwortlich ist – neben Verhaltensunterschieden auf verschiedenen Varianten des Betriebssystems geht es insbesondere darum, dass AsyncTasks dazu neigen, Speicherlecks auszuführen.
Der aus dem Halbleiterbereich stammende Vasiliy Zukanov befasst sich seit einigen Jahren mit Android-Entwicklung und bringt ob seiner Erfahrungen mit Assembler und VHDL (wie der Autor dieser Zeilen) einen sehr eigenen Blickwinkel auf Softwareprobleme mit. Zur Demonstration des am häufigsten auftretenden Problems verwendet er unter https://www.techyourchance.com/asynctask-deprecated/ die folgende Deklaration:
 
@Override
public void onStart() {
    super.onStart();
    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... voids) {
         int counter = 0;
         while (true) {
          Log.d("AsyncTask", "count: " + counter);
             counter ++;
         }
      }
    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
 
Dynamisch erstellte innere Klassen haben in Java die unangenehme Eigenschaft, einen Verweis auf ihr Elternobjekt zu halten. Daraus folgt, dass der AsyncTask die Abtragung der Activity behindert, weil er nie beendet wird. Würde der Entwickler ihn in Cancellation-Szenarios deaktivieren, so träte das Problem nicht auf. Google hat - so zumindest Zukanov - diese Wahrnehmung bis zu einem gewissen Grad befeuert. Wie einen nicht-statischen AsyncTask in Android Studio anlegt und danach die Lint-Codeüberprüfung loslässt, bekommt prinzipiell einen auf Speicherlecks hinweisenden Fehler. Außer Frage steht allerdings, dass man derartige Speicherlecks auch ohne die AsyncTask-Klasse hinbekommt. Der klassische Spruch von Herb Sutter, dass es in der schönen neuen Welt der Mehrkern-Rechnersysteme kein kostenloses Mittagessen gibt, gilt auch unter Java uneingeschränkt.
Im Fall von AsyncTask gilt allerdings, dass die Klasse einige besondere Probleme mitbringt. Wer sich mit Multithreading nicht auskennt, wird ob der API zu abenteuerlichen Konstrukten verleitet. Google selbst ist davon übrigens ebenfalls nicht gefeit. So gab es einen Absturz der offiziellen Android-Settings-Applikation, der auf ein Fehlverhalten beziehungsweise eine Fehlverwendung des AsyncTasks zurückzuführen war. Es gilt in der Java-Welt als guter Ton, wenn man in den Depreciation-Notes einen Hinweis anfügt, was der von der Abkündigung betroffene Entwickler stattdessen tun soll. Im Fall des AsyncTask ist die Sache geradezu lachhaft. Google empfiehlt ernsthaft, die vom einst gehassten Konkurrenten im offiziellen Java-Standard eingebundenen Parallelitätsprimitiva zu verwenden:
 
@deprecated Use the standard <code>java.util.concurrent</code> or
<a href="https://developer.android.com/topic/libraries/architecture/coroutines">
Kotlin concurrency utilities</a> instead.
 
Ob Google den AsyncTask allerdings mit Erscheinen von Android elf über den Jordan schicken wird, ist zu bezweifeln. Erstens würde diese Maßnahme einen absoluten Aufstand der Entwickler auslösen, der die von Permission-System und Co. gemachten Änderungen in den Schatten stellen würde.
Hemmnis zwei ist, dass Google den AsyncTask im Betriebssystem selbst an vielerlei Stellen verwendet. Wer dies nicht glaubt und mehr als 500 GByte freien Speicherplatz hat, kann dies auch selbst überprüfen. Hierzu müssen wir im ersten Schritt das von Google bereitgestellte Repo-Werkzeug herunterladen. Es handelt sich dabei um ein Shell-Skript, das als eine Art Makler zwischen Git und Co. fungiert:
 
tamhan@TAMHAN18:~$ mkdir bin
tamhan@TAMHAN18:~$ PATH=~/bin:$PATH
tamhan@TAMHAN18:~$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
tamhan@TAMHAN18:~$ chmod a+x ~/bin/repo
tamhan@TAMHAN18:~$
 
Googles Repo-Entwickler erweisen sich insofern als unflexibel, als sie das Vorhandensein eines bin-Verzeichnisses im Home-Ordner des gerade angemeldeten Benutzers erwarten. Wer die Path-Konfiguration seiner Workstation nicht anpassen möchte, kann nach dem hier gezeigten Schema vorgehen. Beachten Sie dabei allerdings, dass die Deklaration nur im gerade geöffneten Terminalfenster gültig bleibt.
Wechseln Sie im nächsten Schritt in ein Verzeichnis mit ausreichend Speicherplatz und konfigurieren Sie Ihren Git-Client mit persönlichen Informationen. Dies ist nur dann erforderlich, wenn sie ihrem System vorher noch nicht mitgeteilt haben, wer sie sind:
 
tamhan@TAMHAN18:~$ cd /media/tamhan/Elements1/AOSPPlace/
tamhan@TAMHAN18:/media/tamhan/Elements1/AOSPPlace$ git config --global user.name "Your Name"
tamhan@TAMHAN18:/media/tamhan/Elements1/AOSPPlace$ git config --global user.email "you@example.com"
tamhan@TAMHAN18:/media/tamhan/Elements1/AOSPPlace$
 
Im nächsten Schritt müssen Sie Repo zum Herunterladen des Android-Quellcodes vorbereiten. Hierzu dient das folgende Kommando, dessen Abarbeitung eine Minute Zeit in Anspruch nimmt:
 
tamhan@TAMHAN18:/media/tamhan/Elements1/AOSPPlace$ repo init -u https://android.googlesource.com/platform/
manifest -b android-9.0.0_r49
. . .
repo has been initialized in /media/tamhan/Elements1/AOSPPlace
 
Nach dem erfolgreichen Herunterladen fragt das Werkzeug noch, ob sie die Farbdarstellung aktivieren möchten. Der Autor dieser Zeilen arbeitet in den folgenden Schritten in Vollfarbe (Bild 2).
Google hat ein Herz für Farbenblinde (Bild 2)
Quelle: Hanna
Der eigentliche Download erfolgt dann über einen Synchronisationsbefehl:
 
tamhan@TAMHAN18:/media/tamhan/Elements1/AOSPPlace$ time repo sync -j6 -qc
Über den Parameter -j dürfen sie dabei festlegen, wie viele parallele Threads ihre Workstations verwenden darf. Das übergeben von -qc reduziert die Anzahl der in der Kommandozeile ausgegebenen Statusinformationen.
In der Praxis reicht es aus, die Hälfte der auf Ihrem Rechner zur Verfügung stehenden Kerne zuzuweisen. Das beschränkende Element ist im allgemeinen die Internetverbindung und nicht die Rechenleistung Ihrer Maschine. Die Übergabe von -qc ist nicht immer vernünftig. Wenn sie die Dateien auf einem NTFS-Volume ablegen, sollten Sie das Parameter weglassen, um die Ausführung ein wenig zu verlangsamen. Sinn dieser auf den ersten Blick paradox klingenden Vorgehensweise ist, dass die Dateisystemtreiber von Linux beim Zugriff auf NTFS-Dateisysteme mitunter aus dem Tritt kommen, wenn sie in kurzer Zeit sehr viele Anfragen vorgesetzt bekommen.
Dank des Voranstellen des Time-Kommandos erfahren wir zudem, wie lang die Abarbeitung des Befehls gedauert hat:
 
Real    205m18,109s
User    154m36,306s
Sys    106m44,813s
 
Beachten Sie, dass die Arbeitsgeschwindigkeit des Kommandos nicht nur von ihrer Workstation abhängt. Sind Googles Server überlastet, so kann der Download mehr Zeit in Anspruch nehmen.

Schneller Test

Nach dem Download der rund 25 GByte an Informationen sind wir zur Durchführung eines schnellen und unwissenschaftlichen Tests befähigt. Geben Sie das folgende Kommando ein, um die Anzahl der gefundenen AsyncTask-Aufrufe im Gesamt-Repository zu ermitteln:
 
tamhan@TAMHAN18:/media/tamhan/Elements/AOSPPlace3$ grep -r "AsyncTask" . | wc -l
7901
 
Zur Erklärung: als erstes bekommt das Grep-Kommando den Befehl, das vorliegende Dateisystem rekursiv nach Elementen zu durchsuchen, die den gewünschten Suchstring enthalten. Im rohen Zustand lässt sich dieses Kommando übrigens ebenfalls ausführen; es liefert in diesem Fall nach dem in Bild 3 gezeigten Schema Informationen über gefundene Dateien.
Grep zeigt sich von Haus aus durchaus gesprächig (Bild 3)
Quelle: Hanna
Da die einzelnen Treffer allerdings nicht interessieren, leiten wir die Ausgabe stattdessen über ein Pipe-Symbol an das wc-Kommando weiter. Sein Name steht für Word Count es liefert die Anzahl der gefundenen Wörter.
Auch hier gilt übrigens, dass Sie ob der großen Datenmenge einiges an Geduld mitbringen müssen, insbesondere dann, wenn die Dateien auf einer externen Festplatte liegen. Aus der immens hohen Anzahl der Treffer lässt sich ableiten, dass Google den AsyncTask wahrscheinlich nicht hart deaktivieren wird. Die dadurch entstehende Menge an Änderungen wäre sowohl Aktionären als auch dem Management nur schwer zu verkaufen. Trotzdem gibt es einige Möglichkeiten, um dem Problem des AsyncTask umzugehen.

AsyncTask umgehen

Insbesondere im akademischen Bereich gilt es als unschön, wenn Code Deprecated-Elemente verwendet. Findet in ihrem Unternehmen ein derartiger Architekt und eine derartige Vorgabe, so ist Refactoring unbedingt erforderlich. Der einfachste Weg zur Umgehung des Problems macht sich die Situation zu Nutze, dass AsyncTask seit jeher als eine Art Wrapper um die weiter oben besprochenen Java-Parallelisierungskonstrukte ausgeführt ist. Öffnen Sie die URL https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/os/AsyncTask.java, oder suchen Sie nach einer anderen Version von AsyncTask.java, die den Bedürfnissen ihrer Applikation entspricht.
Passen Sie im nächsten Schritt sowohl die Paket-Deklaration als auch die Namen der Klasse an. Der Autor würde den Code beispielsweise in das Paket tamoggemon.utils verschieben und die Klasse AsyncTask in Tams AsyncTask umbenennen.
Im nächsten Schritt können Sie dazu übergehen, in den diversen Codedateien ihrer Applikation die Import-Statements und Klassennamen anzupassen. Einen kurzen Test später wird alles problemlos funktionieren. Das Auswerfen der Deprecated-Warnungen taugt dabei übrigens als eine Art Pfad, der sie zum schnelleren Auffinden der änderungsbedürftigen Stellen befähigt.

Und jetzt zu Fuß

Möchte man den AsyncTask aus irgendeinem Grund wirklich aus dem Programm heraushaben, so stehen mehrere Methoden zur Verfügung. Die wahrscheinlich am wenigsten elegante, aber wirksamste - und für normale Java-Programmierer verständlichste - Vorgehensweise ist die Verwendung eines gewöhnlichen Threads.
Seine Anwendung möchte ich anhand eines vorgefertigten Android-Beispiels demonstrieren: Starten Sie Android Studio und schließen Sie ein eventuell geladenes Projekt, um den Startassistenten auf den Bildschirm zu holen. Entscheiden Sie sich dort für die Option Import an Android Code sample, um das Laden eines der von Google bereitgestellten Beispiele zu befähigen. Das von uns gewünschte Beispiel hört auf den Namen Network Connect und präsentiert sich wie in Bild 4.
Das von Google bereitgestellte Beispiel hört auf den Namen Network Connect (Bild 4)
Quelle: Hanna
Das eigentliche Deployment des Samples erfolgt im Großen und Ganzen so, wie sie es von Android Studio kennen. Eine kleine Besonderheit ist die Ausgabe der folgenden Fehlermeldung:
 
The project 'NetworkConnect' is not a Gradle-based project
More Information about migrating to Gradle
 
Hierbei handelt es sich um ein Problem mit Beta-Versionen von Android Studio. Google hat die Struktur der hauseigenen Sample-Repositories vor einiger Zeit angepasst, weshalb ältere Versionen beim Herunterladen von Beispielen keine kompilationsfähigen Projektskelette erzeugen. Der einfachste Weg zur Lösung des Problems besteht darin, ein Update einzuspielen. Der Autor arbeitet in den folgenden Schritten mit Version 3.5 der IDE.
Sonst sind wir an dieser Stelle mit den Vorbereitungen fertig. Als nächste Aktion wollen wir den AsyncTask aus dem Programm herausbekommen, ohne dabei seine Lauffähigkeit zu bedrohen.
Das wichtigste Werkzeug beim Schreiben von AsyncTask-Code ist die in Bild 5 gezeigte Aufstellung. Sie informiert uns darüber, welche Aufgaben der AsyncTask in einem eigenen Thread ausführt und welche Aufgaben im GUI-Thread ablaufen.
Diese Tabelle kann für Entwickler beim AsyncTask-Umgehen sehr hilfreich sein (Bild 5)
Quelle: Hanna
Als nächste Aufgabe erstellen wir eine neue Thread-Klasse, in der wir den zur Kommunikation vorgesehenen Callback als privates Member anlegen. Die Methode Run ist ebenfalls erforderlich. Sie nimmt die Payload auf, die später asynchron ausgeführt wird:
 
public class WorkerThread extends Thread {
    private DownloadCallback mCallback;
    @Override
    public void run() {
               
    }
}
 
An dieser Stelle findet sich eine bösartige Falle für Anfänger. Wer die Run-Methode direkt aufruft, spawnt keinen neuen Thread. Die Klasse arbeitet nur dann asynchron, wenn wir sie wie weiter unten besprochen dediziert aktivieren.
Vorher wollen damit beginnen, die im AsyncTask befindliche Logik zu übertragen. Die Methoden onPreExecute und onPostExecute, die außerhalb des GUI-Thread ablaufen, wandern eins zu eins in die neue Arbeiterklasse:
 
public class WorkerThread extends Thread {
  protected void onPreExecute() {
    . . .
  protected void onPostExecute(Result result) {
   . . .
 
Google implementiert in Network Connect auch die Möglichkeit zum Abbrechen der Netzwerkkommunikation. Wir wollen uns mit diesem Feature im Moment nicht aufhalten, weshalb sie die relevanten Teile des Codes einfach eliminieren.
Bei der Anpassung der Funktion downloadURL bekommen wir erstmals Probleme. Sie ist für das Herunterladen verantwortlich, nutzt aber auch die im AsyncTask angelegte Möglichkeit zum Abfeuern von Fortschrittsinformationen, der diese Aufrufe direkt verarbeiten kann.
Hierzu dient die Methode publishProgress, die in DownloadURL mehrfach vorkommt. Aus Platzgründen wollen wir uns hier nur dem ersten Vertreter der Gattung ansehen:
 
private String downloadUrl(URL url) throws IOException {
  InputStream stream = null;
  HttpsURLConnection connection = null;
  String result = null;
  try {
      . . .
connection.connect();
publishProgress(DownloadCallback.Progress.
CONNECT_SUCCESS);
 
Einer der an AsyncTask gerichteten Kritikpunkte ist, dass die Klasse sehr viele Methoden mit variabler Parameteranzahl mitbringt und dementsprechend schwer zu handhaben ist. Im Fall unserer problematischen Methode hört die Gegenstelle auf den Namen onProgressUpdate:
 
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
    if (values.length >= 2) {
      mCallback.onProgressUpdate(values[0], values[1]);
    }
}
 
An sich ist die Lösung der Aufgabe an dieser Stelle einfach. Wir müssen nur dafür sorgen, dass die Payload im GUI-Thread ausgeführt wird. Dieser ist logischerweise ein Singleton, weshalb die Aufgabe per se lösbar ist.
Blickt man allerdings auf die unter https://stackoverflow.com/questions/12850143/android-basics-running-code-in-the-ui-thread/12850234 bereitstehende und sehr lebendige Diskussion auf StackOverflow, so bemerkt man, dass der Gutteil der dort verwendeten Lösungen die Payload über eine Methode an den GUI-Thread übergibt, die eine MainActivity-Instanz voraussetzt. Damit ist bewiesen, dass man Speicherlecks bei mehrkerniger Programmierung auch dann provozieren kann, wenn man ohne AsyncTask arbeitet.

Auslieferung eines Jobs

Wie dem auch sei, bietet sich die folgende komplett unabhängige Methode zur Auslieferung eines Jobs an:
 
new Handler(Looper.getMainLooper()).post(new Runnable() {
    @Override
    public void run() {
        Log.d("UI thread", "I am the UI thread");
    }
});
 
Die praktische Erfahrung lehrt, dass der Gutteil der AsyncTask-Anwendungsfälle ohne die multiplen Parameter auskommt. Für unser Beispiel reicht deshalb folgender Code aus:
 
protected void publishProgress(final int a, final int b) {
  new Handler(Looper.getMainLooper()).
  post(new Runnable() {
    @Override
    public void run() {
    mCallback.onProgressUpdate(a, b);
    }
    });
}
 
Weil keine Regel ohne Ausnahme ist, müssen wir uns dann noch der Deklaration publishProgress(DownloadCallback.Progress.CONNECT_SUCCESS) zuwenden. Fügen sie einfach einen weiteren numerischen Parameter hinzu, um folgendes Ergebnis zu erhalten:
 
publishProgress(DownloadCallback.Progress.
CONNECT_SUCCESS, 0);
 
Damit bleibt nur noch die Adaption der Hauptarbeit, die in der Funktion doInBackground unterkommt. Wir wollen sie anfangs unverändert abdrucken, um anhand ihrer Applikationsstruktur Überlegungen anstellen zu können:
 
protected Result doInBackground(String... urls) {
    Result result = null;
    if (!isCancelled() && urls != null && urls.length >
    0) {
 
Die erste Aktion unseres Programms besteht darin, zu überprüfen, ob die Abarbeitung des AsyncTasks abgebrochen wurde. Da wir diese Funktion hier nicht nutzen wollen, können wir diesen Teil des Codes wegfallen lassen.
Als nächstes beginnt das Herunterladen der diversen Informationen. Hierbei bekommen wir es abermals mit einem Parameter mit einer variablen Zahl zu tun:
 
String urlString = urls[0];
try {
    URL url = new URL(urlString);
    String resultString = downloadUrl(url);
 
Zu guter Letzt retourniert die Methode einen Wert an das AsyncTask-Framework. Dieses kümmert sich dann darum, die Informationen an den Aufrufer zurückzugeben:
 
if (resultString != null) {
    result = new Result(resultString);
} else {
    throw new IOException("No response
    received."); }
} catch(Exception e) {
    result = new Result(e); }
}
return result;
}
 
Da wir es hier mit einem eher kleinen Beispiel zu tun haben, realisieren wir die Parameter-Übergabe einfach durch öffentliche Felder. Neben dem bekannten Callback-Objekt legen wir nun ein String-Array an:
 
public class WorkerThread extends Thread {
    public DownloadCallback mCallback;
    public String urls[];
 
Im nächsten Schritt müssen wir uns darum kümmern, denn AsyncTask-Lebenszyklus in unserer hauseigenen Klasse abzubilden. Dies ist insofern schwierig, als wir in PreExecute und PostExecute Code ausführen, der vor oder nach dem Hauptcode auszuführen ist.
Wir wollen uns an dieser Stelle damit zufrieden geben, dass der Anstoß der Payloads erfolgt, bevor wir mit der eigentlichen Abarbeitung des Codes beginnen:
 
@Override
public void run() {
   new Handler(Looper.getMainLooper()).post(new
  Runnable() {
        @Override
        public void run() {
          onPreExecute();
        }
    });
 
Der erfahrene Android- bzw. Multithreading-Entwickler stellt beim ersten Blick auf diese Methode fest, dass nicht sichergestellt ist, dass der Code von preExecute abgearbeitet ist, bevor die Ausführung des eigentlichen Threads bedient. Es wäre vorstellbar, dass der GUI-Thread blockiert ist und es deshalb zu zeitlichen Verschiebungen kommt (Bild 6).
Durch die Blockade des GUI-Threads kommt es zu zeitlichen Verschiebungen (Bild 6)
Quelle: Hanna
Als nächstes folgt jedenfalls die Abarbeitung der eigentlichen Kommunikations-Payload, die das von Google geforderte HTML-Snippet aus dem Internet herunterlädt:
 
Result result = null;
if (urls != null && urls.length > 0) {
    String urlString = urls[0];
    try {
        URL url = new URL(urlString);
        String resultString = downloadUrl(url);
        if (resultString != null) {
            result = new Result(resultString);
        } else {
            throw new IOException("No response
            received.");
        }
    } catch(Exception e) {
        result = new Result(e);
    }
}
 
Damit haben wir allerdings ein weiteres Problem. Ein einfacher Aufruf von return reicht nun nicht mehr zur Rückgabe von Ergebnissen aus, da uns die Support-Infrastruktur von AsyncTask ja nicht mehr zur Verfügung steht. Stattdessen müssen wir nun den zurückzugebenden Wert in eine finale Variable kopieren, die danach per PostExecute ausliefern:
 
final Result rx = result;
new Handler(Looper.getMainLooper()).post(new Runnable() {
    @Override
    public void run() {
        onPostExecute(rx);
    }
});
}
 
Anders als bei OnPreExecute gibt es hier übrigens kein Risiko anti-paralleler Abarbeitung. Post() kann das Runnable ja erst dann im GUI-Thread anmelden, wenn die eigentliche Arbeit schon durchgelaufen ist.
Damit sind wir mit dem lokalen Teil der Anpassungen fertig. Unsere nächste Aufgabe ist das Anpassen des Fragments, das den Task zur Ausführung bringt. Hierzu sind im ersten Schritt Anpassungen der globalen Membervariablen erforderlich:
 
public class NetworkFragment extends Fragment {
    public static final String TAG = "NetworkFragment";
    public WorkerThread myWorker;
 
Einer der schönen Aspekte des Fragment-Lebenszyklus ist, dass sich der Entwickler mit einer ganzen Gruppe von Ereignissen herumärgern darf. Hier eine Reihe von Housekeeping-Funktionen, die bei Tests des Autors dieses Artikels problemlos funktionierten:
 
@Override
public void onAttach(Context context) {
super.onAttach(context);
    myWorker = new WorkerThread();
    myWorker.mCallback = (DownloadCallback)context;
}
@Override
public void onDetach() {
    super.onDetach();
    myWorker.mCallback = null;
}
 
Da wir – wie besprochen - keine Cancellation implementieren, kommentieren wir den Korpus dieser Methode einfach aus:
 
public void cancelDownload() {
    /*if (mDownloadTask != null) {
        DownloadTask.cancel(true);
        mDownloadTask = null;
    }*/
}
 
Zur Fertigstellung brauchen wir dann nur noch die Methode StartDownload, die den eigentlichen Download-Prozess anstoßen wird:
 
public void startDownload() {
    String[] urls = new String[1];
    urls[0]=mUrlString;
    myWorker.urls= urls;
    myWorker.start();
}
 
Wichtig ist an dieser Stelle, dass sie die Payload auf keinen Fall durch Aufruf von run() aufrufen dürfen. Die in der JVM-Thread-Klasse implementierte Funktion start() kümmert sich um den eigentlichen Aufbau. Da es sich dabei um absolutes Antipattern handelt, hier nochmals der nicht erwünschte Code:
 
public void startDownload() {
    myWorker.run();
}
 
An dieser Stelle können Sie unser Programm in den Emulator schicken und den Knopf anklicken. Es wird - wie in Bild 7 - Informationen aus dem Internet herunterladen und diese anzeigen.
Es geht auch ohne AsyncTask (Bild 7)
Quelle: Hanna
Diverse Veranstalter verdienen seit Jahren gut an Kongressen zu paralleler Programmierung. Es ist offensichtlich, dass der oft zitierte Normalentwickler mit parallel ablaufendem Code seine liebe Not hat. Das gilt insbesondere dann, wenn er nicht aus dem Hardwaregeschäft kommt und niemals einen der in Bild 8 gezeigten Logikanalyse-Bildschirme vor sich hatte.
Logikanalyse-Bildschirm: Der eine sieht chaotische Linien, der andere eine harte Schule (Bild 8)
Quelle: Hanna
Zur Entschärfung dieses Problems gibt es seit einiger Zeit Bibliotheken beziehungsweise Design Patterns, die Entwicklern die Arbeit erleichtern sollen. Das am häufigsten verwendete Vorgehenspattern ist dabei die reaktive Entwicklung, die in der Bibliothek RXJava implementiert wird.
Bei der Arbeit mit reaktiver Programmierung orientiert man sich im Prinzip an Datenflüssen: ein Programm ist also eine Art Graph, der auf der einen Seite Informationen annimmt und diese danach in verarbeiteter Form wieder zurückliefert. Der Informationsaustausch zwischen den einzelnen Elementen erfolgt durch das Senden von Messages – eventorientierte Programmierung a la Ted Faison lässt grüßen.
Um uns den Start in die Welt der reaktiven Programmierung zu erleichtern, wollen wir diesmal mit einem leeren Projektskelett anfangen. In den Tests des Autors hört das Projekt auf den Namen RXTest; als Android-Basisversion wurde die Version 6.0 ausgewählt.
Lassen Sie Android Studio das Projektskelett im ersten Schritt wie gewohnt erzeugen, um danach die beiden build.gradle-Dateien zu öffnen. In der Welt der Android-Programmierung hat sich eine als RXJava bezeichnete Bibliothek als Quasistandard etabliert, die wir in den folgenden Schritten verwenden beziehungsweise einbinden wollen. In der zum Modul gehörenden Datei müssen wir die folgenden beiden Bibliotheken einbinden:
 
dependencies {
    implementation fileTree(dir: 'libs',
    include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraint
    layout:1.1.3'
    implementation "io.reactivex.rxjava2:rxjava:2.1.6"
    implementation "io.reactivex.rxjava2:
    rxandroid:2.0.1"
 
Das RXJava-Entwicklerteam teilt seine Bibliothek absichtlich in zwei Komponenten auf. Wer sie unter Android verwenden möchte, muss neben dem Kernel immer auch den Android-spezifischen Teil der Bibliothek zum Download befehlen.
Im Interesse der Sicherheit sei noch einmal daran erinnert, dass auch asynchron erfolgende Netzwerkzugriffe die folgende Permission in der Manifestdatei voraussetzen:
 
<manifest xmlns:android="http://schemas.android.com/
apk/res/android" package="com.tamoggemon.rxnetwork">
<uses-permission android:name=
"android.permission.INTERNET" />
 
Der grundlegende Building Block aller RXJava-basierten Systeme sind Observables. Ein Observable ist dabei eine Klasse, die über ein Interface Ereignisse exponiert, auf das andere Klassen warten können. Für eine erste Demonstration wollen wir durch Verwendung der Just-Methode ein Observable anlegen, das bei Belästigung drei verschiedene Strings zurücklegt. Die hierzu erforderliche Deklaration platzieren wir direkt in der MainActivity:
 
public class MainActivity extends AppCompatActivity {
Observable<String> observable = Observable.just
("A", "B", "C");
 
Von besonderem Interesse ist bei dieser Aktion, dass das Observable eine Temple-Klasse ist. Für uns bedeutet dies, dass sie den Rückgabewert schon im Rahmen der Deklaration festlegen müssen.
Im nächsten Schritt benötigen wir das Objekt, das für das Entgegennehmen der Strings verantwortlich ist. Aus Bequemlichkeit wollen wir uns den Aufwand mit dem Anlegen einer neuen Klassendeklaration ersparen und erzeugen auch den Observer direkt als Element der MainActivity:
 
public class MainActivity extends AppCompatActivity {
Observer<String> observer = new Observer<String>() {
        @Override
        public void onError(Throwable e) {
        }
        @Override
        public void onComplete() {
          Log.d("NMG", "Habe fertig");
        }
        @Override
        public void onSubscribe(Disposable d) {
        }
        @Override
        public void onNext(String response) {
          Log.d("NMG", "OnNext : " + response);
        }
    };
 
RXJava zeigt sich beim Design der Lebenszyklusmethoden kooperativ. Neben der für das Eingehen von Informationen verantwortlichen onNext finden wir hier beispielsweise auch onComplete. Diese Methode wird immer dann aufgerufen, wenn die Ereignisverarbeitung abgeschlossen ist.
Wer RXJava unter Android einsetzt, muss beim Festlegen der import-Statements aufpassen. Das Betriebssystem enthält selbst nämlich eine Gruppe von Klassen, die identische Namen aufweisen. Wer statt der hier abgedruckten Observer-Deklaration versehentlich die von Android inkludiert, bekommt ein nicht lauffähiges Programm:
 
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
 
Zum Testen der beiden Objekte müssen wir dann eine als Subscription bezeichnete Verbindung zwischen Observer und Observable herstellen. Der dazu vorgesehene Code sieht folgendermaßen aus:
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    observable.subscribe(observer);
}
 
An dieser Stelle ist die erste Version des Programms zur Ausführung bereit (Bild 9).
Das Observable hat seine Schuldigkeit getan (Bild 9)
Quelle: Hanna
Unser unter Verwendung der just-Methode erzeugtes Objekt war insofern ein einfacher Patient, als die Rückgabe der einzelnen in ihm enthaltenen Strings keine nennenswerte Latenz verursachte. Wir hätten im Prinzip auch auf einen Array-Körper setzen können. Unsere nächste Aufgabe besteht darin, die von onNext zu retournieren Werte durch einen Berechnungsprozess zu erledigen, der nicht sofort abgeschlossen werden kann.
Zur Erzeugung ist ein kleiner Kunstgriff erforderlich. Entwickler erzeugen im allgemeinen im ersten Schritt eine Methode, die als Rückgabewert ein Observable vom gewünschten Typ aufweist. Innerhalb ihrer Ausführung liefert man dann den Wert von create zurück, die ihrerseits eine Instanz der Klasse ObservableOnSubscribe entgegennimmt:
 
Observable<String> getworkerObservable()
{
    return Observable.create(new
    ObservableOnSubscribe<String>() {
        @Override
        public void subscribe(ObservableEmitter<String>
        e) throws Exception {
          Thread.sleep(20000);
          e.onNext("Hallo!");
        }
    });
}
 
ObservableOnSubscribe ist dabei eine in RXJava enthaltene Hilfsklasse, die ihrerseits die subscribe-Methode exponiert. In ihr bringt der Entwickler dann die Payload unter, die für die Bereitstellung der weiter oben an just übergebenen Strings verantwortlich ist. Unser kleines Beispiel kommt anfangs damit aus, eine längere Denkpause einzulegen und danach über das Emitterobjekt den String Hallo Aufrufer zurückzuliefern. Zum Testen des Programms müssen wir die onCreate-Methode unserer MainActivity nach dem folgenden Schema anpassen:
 
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
    Observable myE = getworkerObservable();
    myE.subscribe(observer);
}
 
Anstatt wie bisher das von just erzeugte Observable zu verwenden, nutzen wir nun eine von getWorkerObservable zurückgelieferte Instanz. Schicken Sie das Programm abermals auf den Emulator oder auf ein reales Gerät und beobachten Sie den Programmstart aufmerksam.
Es wird Ihnen auffallen, dass bis zum Erscheinen der von Android Studio angelegten Meldung am Bildschirm einiges an Zeit vergeht. Das informiert uns darüber, dass der Code - von Haus aus - am-GUI-Thread ausgeführt wird. Die Verwendung von RXJava hat uns bisher also nur insofern Vorteile gebracht, als sie die Implementierung von reaktiver Programmierung ermöglichte.
Die stupid-brutale Lösung des vorliegenden Problems bestünde darin, den Aufruf der subscribe-Methode in einen eigenen Thread zu packen. Dies würde in der Theorie funktionieren. Bei Betrachtung des großen Ganzen ist aber nichts gewonnen, da sich ein Normalentwickler nun abermals mit Threading auseinandersetzen darf.
Das RXJava-Entwicklerteam begegnet diesem Problem durch die Einführung von als Scheduler bezeichneten Klassen. Ein Scheduler - echtzeitbetriebssystemerfahrene Programmierer kennen den Begriff mit Sicherheit – ist ein Stück Logik, das sich um die Zuteilung von Aufgaben an Prozessoren beziehungsweise Prozessorressourcen kümmert. Bei der Arbeit mit RXJava wird der Entwickler dann einen oder mehrere Scheduler in das Framework einschreiben, um die gewünschte Art der Verarbeitung zu festzulegen.
Neben den im Haupt-Paket enthaltenen Schedulern bekommen wir es bei der Arbeit mit Android zudem mit zwei Zusatz-Kandidaten zu tun, die im Paket AndroidSchedulers unterkommen:
 
public final class AndroidSchedulers {
    public static Scheduler mainThread() {
    public static Scheduler from(Looper looper) {
 
Die Methode mainThread ist kritisch, da sie einen Scheduler zurückliefert, der seine Arbeit prinzipiell im Haupt-Thread durchführt und somit den Sinn der Parallelisierung ad absurdum führt. Zur Nutzung unseres neu gewonnenen Wissens müssen wir den Code von onCreate nach dem folgenden Schema anpassen:
 
protected void onCreate(Bundle savedInstanceState) {
    . . .
    Observable myE = getworkerObservable();
    myE.
    subscribeOn(Schedulers.newThread()).
    observeOn(AndroidSchedulers.mainThread()).
    subscribe(observer);
}
 
Vor dem Aufruf der eigentlichen Anmeldefunktion finden sich nun die beiden Funktionen subscribeOn und observeOn. SubscribeOn legt dabei fest, welcher Scheduler für die Abarbeitung der diversen Payloads zuständig ist. Die von ihnen abgefeuerten Ereignisse überspringen Threadgrenzen, weshalb die Festlegung des Listener-Threads über die Methode observeOn erfolgt.
Tabelle 1: Scheduler-Kandidaten
Scheduler Kurzbeschreibung
Schedulers.computation() Verwendet einen Threadpool, der bis zur Anzahl der am Zielgerät vorhandenen Prozessoren anwächst. Daraus folgt, dass dieses Scheduler für Aufgaben ideal geeignet ist, die in den einzelnen Threads hohe Rechenleistung benötigen.
Schedulers.io() Verwendet ebenfalls einen Threadpool, dessen Größe aber offen ist. Das bedeutet, dass die Anzahl der Threads unbegrenzt ist. Diese Vorgehensweise ist immer dann gewünscht, wenn die einzelnen Jobs viel Zeit damit verbringen, auf EA-Geräte zu warten.
Schedulers.newThread() Erzeugt prinzipiell für jede Aufgabe einen neuen Thread, um diesen nachher zu zerstören.
Schedulers.single() Verwendet einen einzelnen Arbeiter-Thread, in dem die Tasks sequenziell ablaufen.
Tabelle 1: Scheduler-Kandidaten
Scheduler Kurzbeschreibung
Schedulers.computation() Verwendet einen Threadpool, der bis zur Anzahl der am Zielgerät vorhandenen Prozessoren anwächst. Daraus folgt, dass dieses Scheduler für Aufgaben ideal geeignet ist, die in den einzelnen Threads hohe Rechenleistung benötigen.
Schedulers.io() Verwendet ebenfalls einen Threadpool, dessen Größe aber offen ist. Das bedeutet, dass die Anzahl der Threads unbegrenzt ist. Diese Vorgehensweise ist immer dann gewünscht, wenn die einzelnen Jobs viel Zeit damit verbringen, auf EA-Geräte zu warten.
Schedulers.newThread() Erzeugt prinzipiell für jede Aufgabe einen neuen Thread, um diesen nachher zu zerstören.
Schedulers.single() Verwendet einen einzelnen Arbeiter-Thread, in dem die Tasks sequenziell ablaufen.
Unsere soeben realisierte Toolchain lässt die Observable-Aufrufe im GUI-Thread der Applikation ablaufen, während die Payloads prinzipiell und immer einen neuen Thread zugewiesen bekommen. Diese Vorgehensweise ist nicht unbedingt erforderlich. Tabelle 1 zeigt häufige Scheduler-Kandidaten. Zur Überprüfung des korrekten Programmverhaltens schicken Sie die geänderte Applikation abermals in Emulator oder Smartphone. Der Start des GUIs erfolgt wieder ohne Verzögerung. Nachdem wir unser Beispiel zum Verschieben der Payload in verschiedene Threads befähigt haben, wollen wir abermals grundlegende Netzwerkkommunikation realisieren. Kehren Sie hierzu in den Code des Network Connect-Beispiels zurück, und beschaffen Sie die beiden Methoden downloadUrl und readStream. Im Interesse der Bequemlichkeit wollen wir uns auf das Zurückgeben des heruntergeladenen Strings beschränken, weshalb wir die diversen Aufrufe der Methode postProgress ersatzlos auskommentieren.

String statt Objekt zurückgeben

In einem produktiven Programm könnten sie statt dem hier verwendeten String ein Objekt zurückgeben, das durch eine zusätzliche Flag darüber informiert, ob es sich hier um endgültige oder temporäre Ergebnisse handelt. Nach dem erfolgreichen Verpacken der beiden Methoden müssen wir noch die eigentliche Logik übernehmen. Sie kommt abermals im ObservableOnSubscribe unter, das sich nun folgendermaßen präsentiert:
 
Observable<String> getworkerObservable(){
    return Observable.create(new
        ObservableOnSubscribe<String>() {
        @Override
        public void subscribe(ObservableEmitter<String>
        e) throws Exception {
 
Analog zur den auf normalen Threads basierenden Beispielen beginnt unsere Arbeit auch hier damit, über die Methode downloadUrl das Herunterladen einiger Bytes der Google-Homepage zu befehligen:
 
String urlString = "https://www.google.com";
try {
  URL url = new URL(urlString);
  String resultString = downloadUrl(url);
 
Im nächsten Schritt überprüfen wir, ob die Netzwerkoperation auch wirklich ein Ergebnis zurückgeliefert hat. Der eigentliche Aufruf von onNext bleibt derweil identisch. Er informiert den Observer über das aufgetretene Ereignis:
 
if (resultString != null) {
  e.onNext(resultString);
} else
{
  throw new IOException("No response received.");
}
 
Im Interesse von gutem Housekeeping sei darauf hingewiesen, dass wir die weiter oben generierte Exception-Logik anpassen müssen. Der Parameter e würde den von außen angelieferten Parameter, der ebenfalls e heißt, überdecken. Zudem rufen wir nach der Abarbeitung der gesamten Payload noch die Methode onComplete auf, um RXJava über das Ende unserer Arbeiten zu informieren:
 
} catch(Exception ex) {
  e.onNext("ERROR!!!");
  }
  e.onComplete();
  }
 });
}
 
An dieser Stelle ist diese Version des Programms zur Ausführung bereit. Schicken Sie sie auf ein Gerät oder in den Emulator und lassen Sie die LogCat-Konsole offen.
Reaktives Netzwerken funktioniert problemlos (Bild 10)
Quelle: Hanna

Bild 10 zeigt, dass auch das RXJava-getriebene Herunterladen von Informationen aus dem Internet problemlos funktioniert.

Fazit

Als Veteran mit langjähriger Entwicklungserfahrung muss der Autor an dieser Stelle klar sagen, dass sich übermäßige Aufregung beziehungsweise Aktivität in Bezug auf die Abkündigung des AsyncTasks derzeit nicht rentiert. Schon die immense Verbreitung der Klasse in Google-eigenen Applikationen sorgt dafür, dass akademischen Modernisierungsbestrebungen ein kräftiges Bollwerk im Weg steht. Wer heute einen AsyncTask verwendet, muss nicht damit rechnen, innerhalb der nächsten ein oder zwei Jahre gravierendere Probleme zu bekommen. Theoretisch könnte Google die AsyncTask-Klasse im Playstore erschnüffeln und ausschließen. In diesem Fall können Sie einfach wie beschrieben einen eigenen Task mitliefern Für Entwickler ist das AsyncTask-Thema trotzdem haarig. Der Autor geht davon aus, dass es nicht sehr lange dauern wird, bis das erste Bewerbungsgespräch die Frage nach dem AsyncTask aufwirft. Erfahrungsgemäß gilt dann, dass die Antwort »nichts tun« nicht zu Consultingaufträgen führt. Zudem ist die eventorientierte beziehungsweise heute reaktive Programmierung ein hochinteressantes Designpattern, das Sie in praktischen Applikationen immer wieder einsetzen können.
Fehler zählen ebenfalls
Beachten Sie, dass unser vorliegendes Programm auch durch Zugriffsprobleme verursachte Fehlerzeilen in die Berechnung einbezieht. Für unser kleines Beispiel ist dies allerdings ohne Relevanz.
Geht es um Kotlin?
Böse Zungen könnten behaupten, dass Google den AsyncTask eliminiert, um Java-Entwickler zu vergrämen und zum Umstieg auf Kotlin zu animieren. Dies wäre Google durchaus zuzutrauen. Wir wollen uns in den folgenden Schritten aber auf Java beschränken und Co-Routinen außen vor lassen.
Achtung vor Fork-Bomben
Die Erzeugung eines einzelnen Threads mag nicht nach Aufwand aussehen, ist aber eine durchaus teure Operation. Beachten Sie dies bei der Auswahl des geeigneten Schedulers - auch Android lässt sich durch zu viele Threads töten.


Das könnte Sie auch interessieren