Kapitel 4. Gleichzeitigkeit in Android

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Dieses Kapitel konzentriert sich nicht speziell auf Kotlin. Stattdessen werden einige der Themen vorgestellt, die dienebenläufige Programmierung umgeben und die im Rest des Buches behandelt werden. Außerdem werden einige Tools vorgestellt, die Android-Entwicklern bereits zur Verfügung stehen, um nebenläufige Aufgaben zu verwalten.

Nebenläufige Programmierung hat den Ruf, eine Art dunkle Kunst zu sein: etwas, das nur von selbsternannten Zauberern gemacht wird und das Anfänger auf eigene Gefahr anfassen. Sicherlich kann das Schreiben korrekter nebenläufiger Programme eine ziemliche Herausforderung sein. Das gilt besonders, weil Fehler in nebenläufigen Programmen nicht immer sofort auffallen. Es ist fast unmöglich, auf Gleichzeitigkeitsfehler zu testen, und es kann extrem schwierig sein, sie zu reproduzieren, selbst wenn sie bekannt sind.

Ein Entwickler, der sich über die Gefahren der gleichzeitigen Programmierung Gedanken macht, tut gut daran, sich an diese drei Dinge zu erinnern:

  • Fast alles, was du tagtäglich tust, außerProgrammieren, ist nebenläufig. In einer parallelen Umgebung kommst du ganz gut zurecht. Nur das Programmieren, bei dem die Dinge nacheinander passieren, ist seltsam.

  • Wenn du versuchst, die Probleme zu verstehen, die die nebenläufige Programmierung mit sich bringt, bist du auf dem richtigen Weg. Selbst ein unvollständiges Verständnis von Nebenläufigkeit ist besser, als Beispielcode zu kopieren und die Daumen zu drücken.

  • Gleichzeitige Programmierung ist genau das, wie Android funktioniert. Jede andere als die trivialste Android-Anwendung erfordert eine gleichzeitige Ausführung. Du kannst also gleich loslegen und herausfinden, was es damit auf sich hat!

Bevor wir uns mit den Einzelheiten befassen, sollten wir einige Begriffe definieren.

Der erste Begriff ist Prozess. Ein Prozess ist ein Speicher, den eine Anwendung nutzen kann, und ein oder mehrere Threads der Ausführung. Der Speicherplatz gehört dem Prozess - keine anderen Prozesse können ihn beeinflussen.1 Eine Anwendung läuft normalerweise als ein einziger Prozess.

Das führt natürlich den zweiten Begriff ein: thread. Ein Thread ist eine Folge von Anweisungen, die der Reihe nach ausgeführt werden.

Und das führt uns zu dem Begriff, der in gewisser Weise den Rest dieses Buches bestimmt: thread-safe. Ein Befehlssatz ist thread-sicher, wenn bei der Ausführung durch mehrere Threads keine mögliche Reihenfolge der von den Threads ausgeführten Befehle zu einem Ergebnis führt, das nicht auch erzielt werden könnte, wenn jeder der Threads den Code vollständig und in einer bestimmten Reihenfolge nacheinander ausführen würde. Das ist ein bisschen schwer zu verstehen, aber es besagt, dass der Code die gleichen Ergebnisse liefert, egal ob die verschiedenen Threads ihn alle gleichzeitig oder nacheinander ausführen. Es bedeutet, dass die Ausführung des Programms zu vorhersehbaren Ergebnissen führt.

Wie kann man also ein Programm thread-sicher machen? Dazu gibt es jede Menge Ideen. Wir möchten eine vorschlagen, die klar, relativ einfach zu befolgen und immer korrekt ist. Befolge einfach eine ziemlich klare, ziemlich einfache Regel, die wir auf ein paar Seiten erläutern werden. Zunächst wollen wir aber genauer erklären, was Thread Safety bedeutet.

Gewinde Sicherheit

Wir haben bereits gesagt, dass thread-sicherer Code bei gleichzeitiger Ausführung durch mehrere Threads kein Ergebnis erzeugen kann, das nicht auch durch eine bestimmte Reihenfolge der einzelnen Threads hätte erzeugt werden können. Diese Definition ist in der Praxis jedoch nicht sehr nützlich: Niemand wird alle möglichen Ausführungsreihenfolgen testen.

Vielleicht können wir das Problem in den Griff bekommen, indem wir uns einige gängige Methoden ansehen, mit denen Code thread-unsicher sein kann.

Thread-Sicherheitsfehler lassen sich in einige Kategorien einteilen. Zwei der wichtigsten sind Atomarität und Sichtbarkeit.

Atomarität

Fast alle Entwickler kennen das Problem der Atomisierung. Dieser Code ist nicht thread-sicher:

fun unsafe() { globalVar++ }

Mehrere Threads, die diesen Code ausführen, können sich gegenseitig stören. Jeder Thread, der diesen Code ausführt, könnte denselben Wert fürglobalVarlesen, z. B. 3. Jeder könnte diesen Wert erhöhen, um 4 zu erhalten, und dann könnte jeder globalVar aktualisieren, um den Wert 4 zu erhalten. Selbst wenn 724 Threads den Code ausführen würden, könnte globalVar, wenn alle mit der Ausführung fertig sind, den Wert 4 haben.

Es ist nicht möglich, dass jeder dieser 724 Threads diesen Code seriell ausführt und globalVar am Ende 4 ist. Da das Ergebnis der gleichzeitigen Ausführung des Codes sich von jedem möglichen Ergebnis der seriellen Ausführung unterscheiden kann, ist dieser Code nach unserer Definition nicht thread-sicher.

Um den Code thread-sicher zu machen, müssen wir die Lese-, Inkrementierungs- und Schreiboperationen für die Variable globalVar zusammen atomar machen. Ein atomarer Vorgang ist ein Vorgang, der nicht von einem anderen Thread unterbrochen werden kann. Wenn die Lese-, Inkrementierungs- und Schreibvorgänge atomar sind, können keine zwei Threads denselben Wert von globalVar sehen und das Programm verhält sich garantiert wie erwartet.

Atomarität ist leicht zu verstehen.

Sichtbarkeit

Unsere zweite Kategorie von Thread-Safety-Fehlern, die Sichtbarkeit, ist viel schwieriger zu erkennen. Auch dieser Code ist nicht thread-sicher:

var shouldStop = false

fun runner() {
    while (!shouldStop) { doSomething() }
}

fun stopper() { shouldStop = true }

Ein Thread, der die Funktion runner ausführt, darf niemals anhalten, auch wenn ein anderer Thread stopper ausführt. Der Thread, derrunner ausführt, bemerkt möglicherweise nie, dass der Wert vonshouldStop auf true geändert hat.

Der Grund dafür ist die Optimierung. Sowohl die Hardware (mit Registern, mehrschichtigen Caches usw.) als auch der Compiler (mit Hoisting, Reordering usw.) tun ihr Bestes, damit dein Code schnell läuft. Um das zu erreichen, sehen die Anweisungen, die die Hardware tatsächlich ausführt, vielleicht gar nicht so aus wie der Kotlin-Quelltext. Während du denkst, dass shouldStop eine einzelne Variable ist, hat die Hardware wahrscheinlich mindestens zwei Repräsentationen für sie: eine in einem Register und eine im Hauptspeicher.

Das willst du auf jeden Fall! Du möchtest nicht, dass die Schleifen in deinem Code vom Zugriff auf den Hauptspeicher abhängen, anstatt Caches und Register zu nutzen. Schneller Speicher optimiert deinen Code, weil er Zugriffszeiten hat, die um mehrere Größenordnungen schneller sind als der Hauptspeicher.

Damit der Beispielcode funktioniert, musst du dem Compiler allerdings erklären, dass er den Wert vonshouldStop nicht im lokalen Speicher (einem Register oder Cache) halten kann. Wenn es, wie vorgeschlagen, mehrere Repräsentationen von shouldStopin verschiedenen Arten von Hardwarespeicher gibt, muss der Compiler sicherstellen, dass der Wert, der in der schnellen, lokalen Repräsentation von shouldStop gehalten wird, in den Speicher geschoben wird, der für alle Threads sichtbar ist. Dies wird als Veröffentlichung des Wertes bezeichnet.

@Synchronized ist der Weg, dies zu tun. Die Synchronisierung teilt dem Compiler mit, dass er dafür sorgen muss, dass alle Seiteneffekte des Codes, der innerhalb des synchronisierten Blocks ausgeführt wird, für alle anderen Threads sichtbar sind, bevor der ausführende Thread den Block verlässt.

Bei der Synchronisierung geht es also nicht so sehr um die Hardware oder um knifflige und komplizierte Kriterien dafür, was geschützt werden muss und was nicht. Synchronisierung ist ein Vertrag zwischen dem Entwickler und dem Compiler. Wenn du deinen Code nicht synchronisierst, steht es dem Compiler frei, jede Optimierung vorzunehmen, die er auf der Grundlage der seriellen Ausführung für sicher halten kann. Wenn es irgendwo einen anderen Code gibt, der auf einem anderen Thread läuft und der Beweis des Compilers ungültig ist, musst du den Code synchronisieren.

Hier ist also die Regel. Wenn du Code schreiben willst, der thread-sicher ist, musst du nur diese eine kurze, klare Regel befolgen. Ich zitiere aus Javas Bibel der parallelen Programmierung,Java Concurrency in Practice:2 Wenn mehr als ein Thread auf eine bestimmte Zustandsvariable zugreift und einer von ihnen möglicherweise darauf schreibt, müssen sie alle ihren Zugriff darauf mit Hilfe der Synchronisierung koordinieren.

Beachte übrigens, dass in diesem Zitat nicht zwischen Lese- und Schreibzugriff für die Synchronisierung unterschieden wird. Wenn nicht garantiert ist, dass niemand den gemeinsamen Zustand verändert, müssen alle Zugriffe, ob lesend oder schreibend, synchronisiert werden.

Das Android Threading Modell

Wie in Kapitel 3 erwähnt, ist eine der Folgen einer MVC-Architektur eine Single-Thread-Benutzeroberfläche (View und Controller). Obwohl eine Multi-Thread-Benutzeroberfläche sehr verlockend erscheint (ein Thread für die View und ein Thread für den Controller würden sicher funktionieren...), wurden Versuche, sie zu bauen, bereits in den 1970er Jahren aufgegeben, als klar wurde, dass sie unweigerlich in einer Reihe von Deadlocks endeten.

Seit der allgemeinen Einführung von MVC ist das Standard-UI-Design eine Warteschlange, die von einem einzigen Thread (in Android der Main- oder UI-Thread) bedient wird. Wie in Abbildung 4-1 dargestellt, werdenEreignisse - sowohlsolche, die vom Benutzer ausgehen (Klicken, Tippen, Eingeben usw.), als auch solche, die vom Modell ausgehen (Animationen, Anforderungen zum Neuzeichnen/Aktualisieren des Bildschirms usw.) - in eine Warteschlange gestellt und schließlich von dem einzelnen UI-Thread der Reihe nach verarbeitet.

pawk 0401
Abbildung 4-1. UI-Thread.

Das ist genau die Art und Weise, wie die Benutzeroberfläche von Android funktioniert. Der Haupt-Thread einer Anwendung (der ursprüngliche Thread des Anwendungsprozesses) wird zu ihrem UI-Thread. Im Rahmen der Initialisierung tritt der Thread in eine enge Schleife ein. Für den Rest der Lebensdauer der Anwendung entnimmt er eine Aufgabe nach der anderen aus der kanonischen UI-Warteschlange und führt sie aus. Da die UI-Methoden immer in einem einzigen Thread ausgeführt werden, versuchen die UI-Komponenten nicht, thread-sicher zu sein.

Das klingt toll, oder? Eine Single-Thread-Benutzeroberfläche und keine Sorgen um die Thread-Sicherheit. Allerdings gibt es ein Problem. Um es zu verstehen, müssen wir unsere Entwicklerhüte ablegen und ein wenig über die Erfahrungen der Endnutzer von Android-Geräten sprechen. Insbesondere müssen wir uns einige Details der Videoanzeige ansehen.

Hinweis

Von einem Deadlock spricht man, wenn jeder Thread eine Ressource besitzt, die der andere benötigt: Keiner von beiden kann vorankommen. Ein Thread könnte zum Beispiel das Widget besitzen, das einen Wert anzeigt, und den Container benötigen, der den Wert enthält, der angezeigt werden soll. Gleichzeitig kann ein anderer Thread den Container besitzen und das Widget benötigen. Deadlocks können vermieden werden, wenn alle Threads die Ressourcen immer in der gleichen Reihenfolge nutzen.

Fallengelassene Rahmen

Aus langjähriger Erfahrung mit Kinofilmen und Fernsehen wissen wir, dass das menschliche Gehirn dazu gebracht werden kann, Bewegungen als kontinuierlich wahrzunehmen, auch wenn sie es nicht sind. Eine Reihe von Standbildern, die schnell hintereinander gezeigt werden, kann dem Betrachter als gleichmäßige, ununterbrochene Bewegung erscheinen. Die Geschwindigkeit, mit der die Bilder angezeigt werden, wird als Bildrate bezeichnet und in Bildern pro Sekunde (fps) gemessen.

Die Standard-Bildrate für Filme ist 24 fps. Das hat während des gesamten Goldenen Zeitalters von Hollywood gut funktioniert. Ältere Fernsehgeräte verwendeten eine Bildrate von 30 fps. Wie du dir vielleicht vorstellen kannst, können schnellere Bildraten das Gehirn noch besser austricksen als langsame. Auch wenn du nicht genau sagen kannst, was du wahrnimmst, wirst du wahrscheinlich einen Unterschied bemerken, wenn du dir ein Video mit einer hohen Bildrate neben einem mit einer niedrigeren Bildrate ansiehst. Das schnellere Video wird sich "flüssiger" anfühlen.

Viele Android-Geräte verwenden eine Bildrate von 60 fps. Das bedeutet, dass der Bildschirm etwa alle 16 Millisekunden (ms) neu gezeichnet wird. Das bedeutet, dass der UI-Thread, der einzelne Thread, der die UI-Aufgaben bearbeitet, alle 16 ms ein neues Bild bereithalten muss, das auf den Bildschirm gezeichnet werden kann. Wenn die Erstellung des Bildes länger dauert und das neue Bild nicht bereit ist, wenn der Bildschirm neu gezeichnet wird, sagen wir, dass das Bild gelöscht wurde.

Es dauert weitere 16 ms, bis der Bildschirm wieder neu gezeichnet wird und ein neues Bild sichtbar wird. Anstelle von 60 fps sinkt die Bildrate auf 30 fps, was nahe an der Schwelle liegt, an der das menschliche Gehirn es bemerkt. Schon ein paar ausgelassene Frames können eine Benutzeroberfläche abgehackt wirken lassen, was manchmal als "Jank" bezeichnet wird.

Betrachte die in Abbildung 4-2 gezeigte Warteschlange von Aufgaben mit der Standard-Renderingrate von Android von 60 fps.

pawk 0402
Abbildung 4-2. Aufgaben in der Warteschlange für den UI-Thread.

Die erste Aufgabe, die Bearbeitung der Zeicheneingabe des Benutzers, dauert 8 ms. Die nächste Aufgabe, das Aktualisieren der Ansicht, ist Teil einer Animation. Damit die Animation flüssig aussieht, muss sie mindestens 24 Mal pro Sekunde aktualisiert werden. Die dritte Aufgabe, die Bearbeitung eines Benutzerklicks, dauert 22 ms. Die letzte Aufgabe im Diagramm ist das nächste Bild der Animation. Abbildung 4-3 zeigt, was der UI-Thread sieht.

pawk 0403
Abbildung 4-3. Ein fallengelassener Frame.

Die erste Aufgabe wird in 8 ms abgeschlossen. Die Animation zeichnet ein Bild in den Anzeigepuffer in 4 ms. Der UI-Thread beginnt dann mit der Bearbeitung des Klicks. Ein paar Millisekunden nach dem Klick wird die Hardware neu gezeichnet und das Bild der Animation ist jetzt auf dem Bildschirm sichtbar.

Leider ist 16 ms später die Aufgabe, den Klick zu verarbeiten, immer noch nicht abgeschlossen. Die Aufgabe zum Zeichnen des nächsten Bildes der Animation, die sich in der Warteschlange dahinter befindet, wurde noch nicht abgearbeitet. Wenn das erneute Zeichnen stattfindet, ist der Inhalt des Anzeigepuffers genau so wie beim vorherigen erneuten Zeichnen. Der Animationsrahmen wurde fallen gelassen.

Hinweis

Computerbildschirme werden in der Regel mit einem oder mehrerenDisplay-Puffern verwaltet. Ein Anzeigepuffer ist ein Speicherbereich, in dem der Benutzercode die Dinge "zeichnet", die auf dem Bildschirm zu sehen sein werden. Gelegentlich wird der Benutzercode im Auffrischungsintervall(ca. 16 ms bei einem Bildschirm mit 60 Bildern pro Sekunde) kurz aus dem Puffer ausgesperrt. Das System verwendet den Inhalt des Puffers zum Rendern des Bildschirms und gibt ihn dann für weitere Aktualisierungen wieder an den Benutzercode frei.

Ein paar Millisekunden später, wenn die Klickbehandlung abgeschlossen ist, bekommt die Animationsaufgabe die Chance, den Anzeigepuffer zu aktualisieren. Auch wenn der Anzeigepuffer jetzt das nächste Bild der Animation enthält, wird der Bildschirm erst nach einigen Millisekunden neu gezeichnet. Die Bildrate für die Animation wurde auf 30 Bilder pro Sekunde halbiert, was gefährlich nahe am sichtbaren Flimmern ist.

Einige neuere Geräte, wie das Pixel 4 von Google, können den Bildschirm mit viel höheren Bildwiederholraten aktualisieren. Bei einer doppelt so hohen Bildwiederholrate (120 fps) muss der UI-Thread, selbst wenn er zwei Bilder hintereinander verpasst, nur 8 ms länger auf die nächste Neuberechnung warten. Das Intervall zwischen den beiden Renderings beträgt in diesem Fall nur etwa 24 ms; das ist viel besser als die 32 ms, die das Fallenlassen eines Bildes bei 60 fps kostet.

Auch wenn eine höhere Bildrate helfen kann, muss ein Android-Entwickler wachsam sein und sicherstellen, dass eine Anwendung so wenig Frames wie möglich fallen lässt. Wenn eine Anwendung mitten in einer aufwendigen Berechnung steckt und diese länger als erwartet dauert, verpasst sie das Zeitfenster für den Neuaufbau und lässt das Bild fallen, und die Anwendung fühlt sich ruckelig an.

Dieses Szenario ist der Grund, warum es absolut notwendig ist, sich mit Gleichzeitigkeit in Android-Anwendungen zu beschäftigen. Einfach ausgedrückt: Die Benutzeroberfläche ist ein Single-Thread und der UI-Thread darf nie länger als ein paar Millisekunden belegt sein

Die einzig mögliche Lösung für eine nicht triviale Anwendung besteht darin, zeitaufwändige Arbeiten - Speicherung und Abruf von Datenbanken, Netzwerkinteraktionen und langwierigeBerechnungen - an einen anderen Thread zu übergeben.

Speicherlecks

Wir haben uns bereits mit einer Komplexität befasst, die durch die Gleichzeitigkeit entsteht: die Thread-Sicherheit. Die komponentenbasierte Architektur von Android fügt eine zweite, ebenso gefährliche Komplexität hinzu:Speicherlecks.

Ein Speicherleck tritt auf, wenn das Objekt nicht freigegeben (garbage-collected) werden kann, obwohl es nicht mehr nützlich ist. Schlimmstenfalls kann ein Speicherleck zu einemOutOfMemoryError und einem Anwendungsabsturz führen. Aber auch wenn es nicht so schlimm wird, kann Speicherknappheit zu häufigeren Speicherbereinigungen führen, die wiederum "Jank" verursachen.

Wie in Kapitel 3 beschrieben, sind Android-Anwendungen besonders anfällig für Speicherlecks, da die Lebenszyklen einiger der am häufigsten verwendeten Komponenten -Activitys, Fragments, Services usw. - nicht unter der Kontrolle der Anwendung stehen. Instanzen dieser Komponenten können nur allzu leicht zu Ballast werden.

Das gilt vor allem in einer Multithreading-Umgebung. Stell dir vor, du überträgst eine Aufgabe auf einen Worker-Thread wie diesen:

override fun onViewCreated(
    view: View,
    savedInstanceState: Bundle?
) {
    // DO NOT DO THIS!
    myButton.setOnClickListener {
        Thread {
            val status = doTimeConsumingThing()
            view.findViewById<TextView>(R.id.textview_second)
                .setText(status)
        }
            .start()
    }
}

Die Idee, die zeitaufwändige Arbeit aus dem UI-Thread auszulagern, ist nobel. Leider hat der vorangehende Code mehrere Fehler. Kannst du sie erkennen?

Erstens: Wie bereits in diesem Kapitel erwähnt, sind Android UI-Komponenten nicht thread-sicher und können nicht von außerhalb des UI-Threads aufgerufen oder geändert werden. Der Aufruf von setTextin diesem Code aus einem anderen Thread als dem UI-Thread ist falsch. Viele Android UI-Komponenten erkennen unsichere Verwendungen wie diese und lösen Ausnahmen aus, wenn sie auftreten.

Eine Möglichkeit, dieses Problem zu lösen, ist die Rückgabe von Ergebnissen an den UI-Thread mit einer der Android-Toolkit-Methoden für sicheres Thread-Dispatch, wie hier gezeigt. Beachte, dass dieser Code immer noch Fehler hat!

override fun onViewCreated(
    view: View,
    savedInstanceState: Bundle?
) {
    // DO NOT DO THIS EITHER!
    myButton.setOnClickListener {
        Thread {
            val status = doTimeConsumingThing()
            view.post {
                view.findViewById<TextView>(R.id.textview_second)
                    .setText(status)
            }
        }
            .start()
    }
}

Damit ist zwar das erste Problem behoben (die UI-Methode setText wird jetzt vom Main-Thread aus aufgerufen), aber der Code ist immer noch nicht korrekt. Obwohl es aufgrund der Unwägbarkeiten der Sprache schwierig ist, das Problem zu erkennen, besteht es darin, dass der Thread, der imClickListener neu erstellt wurde, einen impliziten Verweis auf ein von Android verwaltetes Objekt enthält. Da doTimeConsumingThing eine Methode auf Activity (oder Fragment) ist, hält der Thread, der im Click Listener neu erstellt wurde, einen impliziten Verweis auf dieses Activity, wie in Abbildung 4-4 gezeigt.

pawk 0404
Abbildung 4-4. Eine durchgesickerte Aktivität.

Es wäre vielleicht noch offensichtlicher, wenn der Aufruf vondoTimeConsumingThing alsthis.doTimeConsumingThing geschrieben würde. Wenn du aber darüber nachdenkst, ist es klar, dass es keine Möglichkeit gibt, die MethodedoTimeConsumingThing auf einem Objekt (in diesem Fall eine Instanz von Activity) aufzurufen, ohne einen Verweis auf dieses Objekt zu haben. Jetzt kann dieActivity Instanz kann nun nicht mehr vom Müll befreit werden, solange Runnable auf dem Worker-Thread einen Verweis auf das Objekt hält. Wenn der Thread für eine längere Zeit läuft, ist Activity Speicher ausgelaufen.

Dieses Problem ist wesentlich schwieriger zu lösen als das letzte. Ein Ansatz geht davon aus, dass Aufgaben, die eine solche implizite Referenz garantiert nur für einen sehr kurzen Zeitraum (weniger als eine Sekunde) halten, kein Problem darstellen. Das Android-Betriebssystem selbst erstellt gelegentlich solche kurzlebigen Tasks.

ViewModels und LiveData stellen sicher, dass deine Benutzeroberfläche immer die aktuellsten Daten wiedergibt, und zwar sicher. Zusammen mit den viewModelScope und Coroutines von Jetpack - beides wird in Kürze eingeführt - erleichtern diese Dinge die Kontrolle über den Abbruch von Hintergrundaufgaben, die nicht mehr relevant sind, und sorgen für Speicherintegrität und Thread-Sicherheit. Ohne die Bibliotheken müssten wir uns um all diese Dingeselbst kümmern.

Hinweis

Ein sorgfältiges Design mit den lebenszyklusorientierten, beobachtbaren Containern von Jetpacks ( LiveData ), wie inKapitel 3 beschrieben, kann helfen, sowohl Speicherlecks als auch die Gefahr der Verwendung einer Android-Komponente, die ihren Lebenszyklus abgeschlossen hat, zu vermeiden.

Werkzeuge zur Verwaltung von Threads

Es gibt noch einen dritten Fehler in dem Code, den wir gerade besprochen haben: einen tiefgreifenden Design-Fehler.

Threads sind sehr teure Objekte. Sie sind groß, haben Auswirkungen auf die Speicherbereinigung und der Kontextwechsel zwischen ihnen ist alles andere als kostenlos. Das Erstellen und Zerstören von Threads, wie es der Code im Beispiel tut, ist ziemlich verschwenderisch, unklug und beeinträchtigt wahrscheinlich die Leistung der Anwendung.

Wenn du mehr Threads startest, kann eine Anwendung keineswegs mehr Arbeit erledigen: Eine CPU hat nur so viel Leistung. Threads, die nicht ausgeführt werden, sind einfach eine teure Methode, um Arbeit zu repräsentieren, die noch nicht abgeschlossen ist.

Stell dir zum Beispiel vor, was passieren würde, wenn ein NutzermyButton aus dem vorherigen Beispiel mischen würde. Selbst wenn die Operationen, die jeder der erzeugten Threads ausführt, schnell und thread-sicher wären, würde das Erzeugen und Zerstören dieser Threads die App zum Kriechen bringen.

Eine bewährte Methode für Anwendungen ist eine Thread-Policy: eine anwendungsweite Strategie, die auf der Anzahl der tatsächlich nützlichen Threads basiert und steuert, wie viele Threads zu einem bestimmten Zeitpunkt ausgeführt werden. Eine intelligente Anwendung unterhält einen oder mehrere Pools von Threads, die jeweils einem bestimmten Zweck dienen und jeweils von einer Warteschlange angeführt werden. Der Client-Code, der eine Aufgabe zu erledigen hat, reiht die Aufgaben in die Warteschlange ein, die von den Pool-Threads ausgeführt werden sollen, und ruft bei Bedarf die Ergebnisse der Aufgaben ab.

In den nächsten beiden Abschnitten werden zwei Threading-Primitive vorgestellt, die Android-Entwicklern zur Verfügung stehen: Looper/Handler und Executor.

Looper/Handler

Das Looper/Handler ist ein Rahmenwerk aus kooperierenden Klassen: einLooper, ein MessageQueue und die darauf aufgereihten Messages sowie ein oder mehrere Handlers.

Ein Looper ist einfach ein Java Thread, das durch den Aufruf der Methoden Looper.prepare() undLooper.start() von der Methode run aus aufgerufen wird, wie hier:

var looper = Thread {
    Looper.prepare()
    Looper.loop()
}
looper.start()

Die zweite Methode, Looper.loop(), veranlasst den Thread, in eine enge Schleife einzutreten, in der er seine MessageQueue nach Aufgaben durchsucht, sie eine nach der anderen entfernt und sie ausführt. Wenn es keine Aufgaben gibt, die ausgeführt werden müssen, schläft der Thread, bis eine neue Aufgabe in die Warteschlange gestellt wird.

Hinweis

Wenn du denkst, dass dir das irgendwie bekannt vorkommt, hast du recht. Der UI-Thread von Android ist einfach einLooper, der vom Hauptthread des Anwendungsprozesses erstellt wurde.

Eine Handler ist der Mechanismus, mit dem du Aufgaben in die Warteschlange einerLooperzur Bearbeitung einreihst. Du erstellst eine Handlerwie folgt:

var handler = new Handler(someLooper);

Die Looper des Hauptthreads ist immer über die Methode Looper.getMainLooper zugänglich. Das Erstellen einer Handler, die Aufgaben an den UI-Thread sendet, ist also ganz einfach:

var handler = new Handler(Looper.getMainLooper);

Genau so funktioniert auch die Methode post(), die im vorangegangenen Beispiel gezeigt wurde.

Handlers sind interessant, weil sie beide Enden der Looper's Warteschlange bearbeiten. Um zu sehen, wie das funktioniert, wollen wir eine einzelne Aufgabe durch das Looper/HandlerFramework verfolgen.

Es gibt mehrere Handler Methoden, um eine Aufgabe in die Warteschlange zu stellen. Hier sind zwei davon:

  • post(task: Runnable)

  • send(task: Message)

Diese beiden Methoden definieren zwei leicht unterschiedliche Arten, eine Aufgabe in die Warteschlange zu stellen: das Senden von Nachrichten und das Posten von Runnables. Eigentlich stellt Handler immer eine Message in die Warteschlange. Der Einfachheit halber hängt die post...() Methodengruppe jedoch eineRunnable an die Message an, um sie besonders zu behandeln.

In diesem Beispiel verwenden wir die Methode Handler.post(task: Runnable), um unsere Aufgabe in die Warteschlange zu stellen. Die Handler bezieht einMessage Objekt aus einem Pool, hängt das Runnable an und fügt das Message an das Ende des Looper'sMessageQueue an.

Unsere Aufgabe wartet nun auf ihre Ausführung. Wenn sie den Anfang der Warteschlange erreicht, wird sie von Looper abgeholt und interessanterweise genau an dieselbe Handler zurückgegeben, die sie in die Warteschlange gestellt hat. Dieselbe Handler Instanz, die eine Aufgabe in die Warteschlange stellt, ist auch immer die Instanz, die sie ausführt.

Das kann etwas verwirrend erscheinen, bis du erkennst, dass derHandler Code, der die Aufgabe eingereicht hat, auf jedem beliebigen Anwendungsthread laufen kann. Der Handler Code, der die Aufgabe bearbeitet, läuft jedoch immer auf dem Looper, wie in Abbildung 4-5 gezeigt.

pawk 0405
Abbildung 4-5. Looper/Handler.

Die Methode Handler, die von Looper aufgerufen wird, um eine Aufgabe zu bearbeiten, prüft zunächst, ob Message eineRunnable enthält. Wenn dies der Fall ist - und da wir eine derpost...() Methoden verwendet haben, ist dies bei unserer Aufgabe der Fall - führt Handlerdie Runnable aus.

Hätten wir eine der Methoden send...() verwendet, hätte Handler die Message an seine eigene überschreibbare MethodeHandler.handleMessage(msg: Message) weitergegeben.Eine Unterklasse von Handler würde in dieser Methode das AttributMessage what verwenden, um zu entscheiden, welche bestimmte Aufgabe sie ausführen soll, und die Attribute arg1, arg2 undobj als Aufgabenparameter.

Die MessageQueue ist eigentlich eine sortierte Warteschlange. JedesMessage enthält als eines seiner Attribute den frühesten Zeitpunkt, zu dem es ausgeführt werden kann. In den beiden vorangegangenen Methoden, postund send, wird einfach die aktuelle Zeit verwendet (die Nachricht wird "jetzt" sofort bearbeitet).

Mit zwei anderen Methoden können Aufgaben in eine Warteschlange gestellt werden, um zu einem späteren Zeitpunkt ausgeführt zu werden:

  • postDelayed(runnable, delayMillis)

  • sendMessageDelayed(message, delayMillis)

Aufgaben, die mit diesen Methoden erstellt werden, werden in die MessageQueue einsortiert, um zum angegebenen Zeitpunkt ausgeführt zu werden.

Hinweis

Wie bereits erwähnt, kann Looper nur sein Bestes geben, um eine Aufgabe zur gewünschten Zeit auszuführen. Eine verspätete Aufgabe wird zwar nie vor ihrer Zeit ausgeführt, aber wenn eine andere Aufgabe den Thread in Anspruch nimmt, kann die Aufgabe zu spät ausgeführt werden.

Looper/Handlers sind ein fantastisch vielseitiges und effizientes Werkzeug. Das Android-System macht ausgiebig Gebrauch von ihnen, insbesondere von den send...() Aufrufen, die keine Speicherzuweisung vornehmen.

Beachte, dass Looper Aufgaben an sich selbst übertragen kann. Aufgaben, die ausgeführt und nach einem bestimmten Intervall neu geplant werden (mit einer der Methoden von ...Delayed() ), sind eine der Möglichkeiten, mit denen Android Animationen erstellt.

Beachte auch, dass eine Aufgabe, die nur auf einem bestimmten Looper ausgeführt wird, nicht thread-sicher sein muss, da Looper single-threaded ist. Wenn eine Aufgabe, auch eine asynchrone, nur auf einem einzigen Thread ausgeführt wird, ist keine Synchronisierung oder Reihenfolge erforderlich. Wie bereits erwähnt, hängt das gesamte Android UI Framework, das nur auf dem UI Looper läuft, von dieser Annahme ab.

Testamentsvollstrecker und TestamentsvollstreckerDienstleistungen

Java führte Executorund ExecutorServicein Java 5 ein, als Teil eines neuen Concurrency Frameworks. Das neue Framework bot mehrere übergeordnete Abstraktionen für Gleichzeitigkeit, die es Entwicklern ermöglichten, viele Details von Threads, Sperren und Synchronisierung hinter sich zu lassen.

Ein Executor ist, wie der Name schon sagt, ein Dienstprogramm, das Aufgaben ausführt, die ihm vorgelegt werden. Sein Vertrag ist die einzelne Methodeexecute(Runnable).

Java bietet mehrere Implementierungen der Schnittstelle, jede mit einer anderen Ausführungsstrategie und einem anderen Zweck. Die einfachste davon ist mit der MethodeExecutors.newSingleThreadExecutor verfügbar.

Ein Single-Thread-Executor ist dem im vorherigen Abschnitt untersuchtenLooper/Handler sehr ähnlich: Es handelt sich um eine unbeschränkte Warteschlange vor einem einzelnen Thread. Neue Aufgaben werden in die Warteschlange eingereiht, dann entfernt und der Reihe nach von dem einzelnen Thread ausgeführt, der die Warteschlange bedient.

Looper/Handlers und single-threaded Executors haben jeweils ihre eigenen Vorteile. Ein Looper/Handler ist zum Beispiel stark optimiert, um die Zuweisung von Objekten zu vermeiden. Andererseits ersetzt ein single-threadedExecutor seinen Thread, wenn dieser durch eine fehlschlagende Aufgabe abgebrochen wird.

Eine Verallgemeinerung des Single-Thread-Systems Executor istFixedThreadPoolExecutor: Statt eines einzelnen Threads wird die unbegrenzte Warteschlange von einer festen Anzahl von Threads bedient. Wie das Single-Thread-System Executor ersetzt auch FixedThreadPoolExecutor Threads, wenn Tasks sie beenden. EinFixedThreadPoolExecutor garantiert jedoch nicht die Reihenfolge der Aufgaben und führt sie gleichzeitig aus, wenn die Hardware es zulässt.

Das Single-Thread-Programm Executor ist Javas Äquivalent zu Looper/Handler. Es ähnelt dem Single-Thread-Programm Executor mit dem Unterschied, dass die Warteschlange wie beiLooper/Handler nach der Ausführungszeit sortiert ist. Die Aufgaben werden in zeitlicher Reihenfolge ausgeführt, nicht in der Reihenfolge der Einreichung. Wie bei Looper/Handler können lang laufende Aufgaben natürlich verhindern, dass nachfolgende Aufgaben rechtzeitig ausgeführt werden.

Wenn keines dieser Standardausführungsprogramme deine Anforderungen erfüllt, kannst du eine benutzerdefinierte Instanz vonThreadPoolExecutor erstellen und Details wie die Größe und Reihenfolge der Warteschlange, die Anzahl der Threads im Thread-Pool und wie sie erstellt werden, sowie das Verhalten, wenn die Warteschlange des Pools voll ist, angeben.

Es gibt noch eine weitere Art von Executor, die besondere Aufmerksamkeit verdient -ForkJoinPool . Fork-Join-Pools gibt es aufgrund der Beobachtung, dass ein einzelnes Problem manchmal in mehrere Teilprobleme zerlegt werden kann, die gleichzeitig ausgeführt werden können.

Ein häufiges Beispiel für diese Art von Problem ist das Addieren zweier gleich großer Arrays. Die synchrone Lösung besteht darin, i = 0 .. n - 1 zu iterieren, wobei n die Größe des Arrays ist, und bei jedem i die s[i] = a1[i] + a2[i] zu berechnen.

Es gibt jedoch eine clevere Optimierung, die möglich ist, wenn die Aufgabe in Teile aufgeteilt wird. In diesem Fall kann die Aufgabe in n` Teilaufgaben unterteilt werden, von denen jede s[i] = a1[i] + a2[i] für eine bestimmte i berechnet.

Beachte, dass ein Ausführungsdienst, der Teilaufgaben erstellt, die er selbst bearbeiten will, die Teilaufgaben in eine thread-lokale Warteschlange einreihen kann. Da die lokale Warteschlange hauptsächlich von einem einzelnen Thread genutzt wird, gibt es fast nie Streit um die Warteschlangensperren. Die meiste Zeit gehört die Warteschlange dem Thread - er allein stellt Dinge in die Warteschlange und nimmt sie wieder heraus. Das kann eine ziemliche Optimierung sein.

Stell dir einen Pool dieser Threads vor, jeder mit seiner eigenen schnellen, lokalen Warteschlange. Nehmen wir an, dass einer der Threads seine Arbeit beendet hat und sich in den Leerlauf begeben will, während ein anderer Pool-Thread eine Warteschlange mit 200 Teilaufgaben abzuarbeiten hat. Der untätige Thread stiehlt die Arbeit. Er schnappt sich die Sperre für die Warteschlange des beschäftigten Threads, schnappt sich die Hälfte der Teilaufgaben, legt sie in seine eigene Warteschlange und macht sich an die Arbeit.

Der Trick mit der Arbeitsübernahme ist besonders nützlich, wenn nebenläufige Aufgaben ihre eigenen Unteraufgaben erzeugen. Wie wir sehen werden, sind die Koroutinen von Kotlin genau solche Aufgaben .

Tools zur Verwaltung von Aufträgen

Genauso wie es bei der Produktion von z. B. Autos Größenvorteile geben kann, gibt es wichtige Optimierungen, die den Blick auf das große Ganze eines Systems erfordern. Betrachte die Nutzung des Radios in einem Mobiltelefon.

Wenn eine Anwendung mit einem entfernten Dienst interagieren muss, muss das Telefon, das sich normalerweise im Batteriesparmodus befindet, sein Funkgerät einschalten, sich mit einem nahegelegenen Sendemast verbinden, eine Verbindung aushandeln und dann seine Nachricht übertragen. Da das Aushandeln der Verbindung viel Zeit in Anspruch nimmt, hält das Telefon die Verbindung eine Zeit lang offen. Wir gehen davon aus, dass, wenn eine Netzwerkinteraktion stattgefunden hat, wahrscheinlich weitere folgen werden. Wenn jedoch mehr als eine Minute vergeht, ohne dass das Netz genutzt wird, geht das Telefon wieder in den Ruhezustand über, um Akku zu sparen.

Stell dir nun vor, was passiert, wenn mehrere Anwendungen zu unterschiedlichen Zeiten zu Hause anrufen. Wenn die erste Anwendung ihren Ping sendet, schaltet das Telefon sein Funkgerät ein, handelt die Verbindung aus, sendet eine Nachricht für die Anwendung, wartet ein wenig und geht dann wieder in den Ruhezustand. In dem Moment, in dem es wieder in den Ruhezustand geht, versucht die nächste Anwendung, das Netzwerk zu nutzen. Das Telefon muss sich wieder einschalten, eine neue Verbindung aushandeln und so weiter. Wenn es mehr als eine Handvoll Anwendungen gibt, die dies tun, läuft das Telefonradio praktisch die ganze Zeit mit voller Leistung. Außerdem verbringt es einen Großteil dieser Zeit damit, eine Netzwerkverbindung neu zu verhandeln, die es erst vor ein paar Sekunden unterbrochen hat.

Keine einzelne Anwendung kann diese Art von Problem verhindern. Es braucht einen systemweiten Überblick über die Batterie- und Netzwerknutzung, um mehrere Apps (jede mit ihren eigenen Anforderungen) zu koordinieren und die Lebensdauer der Batterie zu optimieren.

Mit Android 8.0 (API 26+) wurden Beschränkungen für den Ressourcenverbrauch von Anwendungen eingeführt. Zu diesen Einschränkungen gehören die folgenden:

  • Eine Anwendung ist nur dann im Vordergrund, wenn sie eine sichtbare Aktivität hat oder einen Dienst im Vordergrund ausführt. Gebundene und gestartete Services verhindern nicht mehr, dass eine Anwendung beendet wird.

  • Anwendungen können ihr Manifest nicht verwenden, um sich für implizite Broadcasts zu registrieren. Auch für das Senden von Broadcasts gibt es Einschränkungen.

Diese Einschränkungen können es einer Anwendung erschweren, "Hintergrund"-Aufgaben auszuführen: Synchronisierung mit einem entfernten Standort, Aufzeichnung des Standorts und so weiter. In den meisten Fällen lassen sich die Einschränkungen mit JobScheduler oder Jetpack's WorkManager abmildern.

Wenn mittlere bis große Aufgaben mehr als ein paar Minuten in der Zukunft geplant werden müssen, ist es eine bewährte Methode, eines dieser Programme zu verwenden. Es kommt auf die Größe an: Eine Animation alle paar Millisekunden zu aktualisieren oder eine weitere Standortprüfung in ein paar Sekunden zu planen, ist wahrscheinlich eine gute Sache für ein Zeitplanungsprogramm auf Thread-Ebene. Eine Datenbank alle 10 Minuten aus dem Upstream zu aktualisieren, ist definitiv etwas, das mit dem JobScheduler gemacht werden sollte.

JobScheduler

Das JobScheduler ist das Zeitplanungsprogramm von Android, mit dem in Zukunft Aufgaben - möglicherweise auch wiederkehrende Aufgaben - geplant werden können. Es ist sehr anpassungsfähig und optimiert nicht nur die Akkulaufzeit, sondern bietet auch Zugriff auf Details des Systemzustands, die Anwendungen bisher aus Heuristiken ableiten mussten.

Ein JobScheduler Auftrag ist eigentlich ein gebundener Dienst. Eine Anwendung deklariert einen speziellen Dienst in ihrem Manifest, um ihn für das Android-System sichtbar zu machen. Dann plant sie die Aufgaben für den Dienst mit JobInfo .

Wenn die Bedingungen von JobInfoerfüllt sind, bindet Android den Aufgabendienst, so wie wir es in "Gebundene Dienste" beschrieben haben . Sobald die Aufgabe gebunden ist, weist Android den Dienst an, ihn auszuführen und übergibt alle relevanten Parameter.

Der erste Schritt bei der Erstellung einer JobScheduler Aufgabe besteht darin, sie im Anwendungsmanifest zu registrieren. Das geschieht wie hier gezeigt:

<service
    android:name=".RecurringTask"
    android:permission="android.permission.BIND_JOB_SERVICE"/>

Das Wichtigste in dieser Erklärung ist die Berechtigung. Wenn der Dienst nicht mit genau der Berechtigungandroid.permission.BIND_JOB_SERVICE erklärt wird, kannJobScheduler ihn nicht finden.

Beachte, dass der Aufgabendienst für andere Anwendungen nicht sichtbar ist. Das ist kein Problem. Die JobScheduler ist Teil des Android-Systems und kann Dinge sehen, die normale Anwendungen nicht sehen können.

Der nächste Schritt beim Einrichten einer JobScheduler Aufgabe ist die Zeitplanung, wie hier in der Methode schedulePeriodically gezeigt:

const val TASK_ID = 8954
const val SYNC_INTERVAL = 30L
const val PARAM_TASK_TYPE = "task"
const val SAMPLE_TASK = 22158

class RecurringTask() : JobService() {
    companion object {
        fun schedulePeriodically(context: Context) {
            val extras = PersistableBundle()
            extras.putInt(PARAM_TASK_TYPE, SAMPLE_TASK)

            (context.getSystemService(Context.JOB_SCHEDULER_SERVICE)
                as JobScheduler)
                .schedule(
                    JobInfo.Builder(
                        TASK_ID,
                        ComponentName(
                            context,
                            RecurringTask::class.java
                        )
                    )
                        .setPeriodic(SYNC_INTERVAL)
                        .setRequiresStorageNotLow(true)
                        .setRequiresCharging(true)
                        .setExtras(extras)
                        .build()
                )
        }
    }

    override fun onStartJob(params: JobParameters?): Boolean {
        // do stuff
        return true;
    }

    override fun onStopJob(params: JobParameters?): Boolean {
        // stop doing stuff
        return true;
    }
}

Diese spezielle Aufgabe wird alle SYNC_INTERVAL Sekunden ausgeführt, aber nur, wenn genügend Speicherplatz auf dem Gerät vorhanden ist und wenn es gerade an eine externe Stromquelle angeschlossen ist. Das sind nur zwei der vielen Eigenschaften, die für die Planung einer Aufgabe zur Verfügung stehen. Die Granularität und Flexibilität des Zeitplanungsprogramms ist vielleicht die attraktivste Eigenschaft von JobScheduler.

Beachte, dass JobInfo die auszuführende Aufgabenklasse auf die gleiche Weise identifiziert, wie wir das Ziel fürIntent in Kapitel 3 identifiziert haben.

Das System ruft die Methode onStartJob der Aufgabe auf der Grundlage der in JobInfo festgelegten Kriterien auf, wenn die Aufgabe ausgeführt werden kann. Deshalb gibt es das JobScheduler. Da es die Zeitpläne und Anforderungen für alle geplanten Aufgaben kennt, kann es die Zeitplanung global optimieren, um die Auswirkungen, insbesondere auf die Batterie, zu minimieren.

Pass auf! Die Methode onStartJob wird auf dem Hauptthread (UI) ausgeführt. Wenn es sich bei der geplanten Aufgabe um etwas handelt, das mehr als ein paar Millisekunden dauert, muss sie in einem Hintergrund-Thread ausgeführt werden, und zwar mit einer derzuvor beschriebenen Techniken.

Wenn onStartJob true zurückgibt, lässt das System die Anwendung so lange laufen, bis es entweder jobFinished aufruft oder die in JobInfo beschriebenen Bedingungen nicht mehr erfüllt sind. Wenn z. B. das Telefon, auf dem dieRecurringTask im vorherigen Beispiel läuft, von der Stromquelle getrennt wird, würde das System sofort die Methode onStopJob() der laufenden Aufgabe aufrufen, um sie zu benachrichtigen, dass sie aufhören soll.

Wenn eine JobScheduler Aufgabe einen Aufruf an onStopJob()erhält, muss sie anhalten. Die Dokumentation schlägt vor, dass der Task ein bisschen Zeit hat, um aufzuräumen und sauber zu beenden. Leider ist sie ziemlich vage, was genau ein "bisschen" ist. Die Warnung "Du bist allein für das Verhalten deiner Anwendung nach Erhalt dieser Nachricht verantwortlich; wenn du sie ignorierst, wird sich deine Anwendung wahrscheinlich nicht mehr richtig verhalten." ist jedoch ziemlich eindeutig.

Wenn onStopJob() false zurückgibt, wird die Aufgabe nicht erneut geplant, auch wenn die Kriterien in JobInfo erfüllt sind: Die Aufgabe wurde abgebrochen. Eine wiederkehrende Aufgabe sollte immertrue zurückgeben.

WorkManager

Die WorkManager ist eine Android-Jetpack-Bibliothek, die dieJobScheduler umhüllt. Sie ermöglicht es einer einzigen Codebasis, moderne Android-Versionen optimal zu nutzen - also solche, dieJobSchedulerunterstützen - und trotzdem mit älteren Android-Versionen zu arbeiten, die das nicht tun.

Die Dienste, die WorkManager anbietet, und seine API sind ähnlich wie die der JobScheduler, die es umhüllt, aber sie sind einen Schritt weiter von den Details der Implementierung entfernt und eine Abstraktion prägnanter.

Während JobScheduler den Unterschied zwischen einer Aufgabe, die sich periodisch wiederholt, und einer, die einmal läuft, in der Boolean Rückgabe der onStopJob Methode kodiert, machtWorkManager ihn explizit; es gibt zwei Arten von Aufgaben: eine OneTimeWorkRequest und eine PeriodicWorkRequest.

Das Einreihen einer Arbeitsanforderung gibt immer ein Token zurück, ein WorkRequest, das verwendet werden kann, um die Aufgabe abzubrechen, wenn sie nicht mehr notwendig ist.

Die WorkManager unterstützt auch die Erstellung komplexer Aufgabenketten: "Führe dies und jenes parallel aus und führe das andere aus, wenn beide erledigt sind." Diese Aufgabenketten erinnern dich vielleicht sogar an die Ketten, mit denen wir in Kapitel 2 Sammlungen umgewandelt haben.

WorkManager ist die flüssigste und prägnanteste Methode, um zu gewährleisten, dass die notwendigen Aufgaben ausgeführt werden (auch wenn deine Anwendung nicht auf dem Bildschirm des Geräts zu sehen ist), und zwar so, dass der Akku optimal genutzt wird.

Zusammenfassung

In diesem Kapitel haben wir das Threading-Modell von Android und einige Konzepte und Tools vorgestellt, mit denen du es effektiv nutzen kannst. Zusammengefasst:

  • Ein thread-sicheres Programm ist ein Programm, das sich unabhängig davon, wie viele Threads es gleichzeitig ausführen, so verhält, wie es reproduziert werden könnte, wenn dieselben Threads esseriell ausführen würden.

  • Im Android-Threading-Modell ist der UI-Thread für die folgenden Aufgaben zuständig:

    • Zeichnen der Ansicht

    • Versenden von Ereignissen, die aus der Interaktion des Benutzers mit der UI resultieren

  • Android-Programme verwenden mehrere Threads, um sicherzustellen, dass der UI-Thread frei ist, um den Bildschirm neu zu zeichnen, ohne Frames fallen zu lassen.

  • Java und Android bieten mehrere Threading-Primitive auf Sprachebene:

    • Eine Looper/Handler ist eine Warteschlange von Aufgaben, die von einem einzelnen, dedizierten Thread bedient wird.

    • Executors und ExecutionServices sind Java-Konstrukte zur Implementierung einer anwendungsweiten Thread-Management-Richtlinie.

  • Android bietet die Architekturkomponenten JobScheduler und WorkManager, um Aufgaben effizient zu planen.

In den folgenden Kapiteln werden wir uns komplexeren Themen im Bereich Android und Gleichzeitigkeit zuwenden. In ihnen werden wir untersuchen, wie Kotlin die Verwaltung nebenläufiger Prozesse übersichtlicher, einfacher und weniger fehleranfällig macht.

1 Es ist möglich, dass sich Prozesse einen Teil des Speichers teilen (wie bei Binder), aber sie tun dies auf sehr kontrollierte Weise.

2 Goetz et al., 2006. Java Gleichzeitigkeit in der Praxis. Boston: Addison-Wesley.

Get Android mit Kotlin programmieren now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.