Hallo Habr!
Wir erinnern Sie daran, dass wir bereits eine
Vorbestellung für das lang erwartete Buch über die Kotlin-Sprache aus der berühmten Big Nerd Ranch Guides-Reihe haben. Heute haben wir beschlossen, Sie auf eine Übersetzung eines Artikels aufmerksam zu machen, der über Kotlin-Coroutinen und die korrekte Arbeit mit Streams in Android berichtet. Das Thema wird sehr aktiv diskutiert. Der Vollständigkeit halber empfehlen wir Ihnen daher,
diesen Artikel von Habr und
diesen ausführlichen Beitrag aus dem Axmor Software-Blog zu lesen.
Das moderne Wettbewerbs-Framework in Java / Android verursacht Rückrufe und führt zu Blockierungszuständen, da Android keine relativ einfache Möglichkeit bietet, die Thread-Sicherheit zu gewährleisten.
Kotlin Coroutinen sind ein sehr effektives und vollständiges Toolkit, das die Verwaltung des Wettbewerbs viel einfacher und produktiver macht.
Pause und Block: Was ist der Unterschied?Coroutinen ersetzen keine Threads, sondern bieten einen Rahmen für deren Verwaltung. Die Philosophie von corutin besteht darin, einen Kontext zu definieren, in dem Sie
warten können, bis Hintergrundvorgänge abgeschlossen sind, ohne den Hauptthread zu blockieren.
Corutins Ziel ist es in diesem Fall, auf Rückrufe zu verzichten und den Wettbewerb zu vereinfachen.
Einfachstes BeispielNehmen wir zunächst das einfachste Beispiel: Führen Sie coroutine im Kontext von
Main
(Hauptthread) aus. Darin extrahieren wir das Bild aus dem
IO
Stream und senden dieses Bild zur Verarbeitung zurück an
Main
.
launch(Dispatchers.Main) { val image = withContext(Dispatchers.IO) { getImage() }
Der Code ist als Single-Thread-Funktion einfach. Während
getImage
im zugewiesenen Pool von
getImage
Threads ausgeführt wird, ist der Hauptthread außerdem frei und kann jede andere Aufgabe übernehmen! Die Funktion withContext hält die aktuelle Coroutine an, während ihre Aktion ausgeführt wird (
getImage()
). Sobald
getImage()
zurückkehrt und der
looper
aus dem Hauptthread verfügbar wird, nimmt
imageView.setImageBitmap(image)
die Arbeit im Hauptthread wieder auf und ruft
imageView.setImageBitmap(image)
.
Das zweite Beispiel: Jetzt müssen zwei Hintergrundaufgaben erledigt werden, damit sie verwendet werden können. Wir werden das Duett async / await verwenden, damit diese beiden Aufgaben parallel ausgeführt werden, und ihr Ergebnis im Hauptthread verwenden, sobald beide Aufgaben fertig sind:
val job = launch(Dispatchers.Main) { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } job.join()
async
ähnelt dem
launch
, gibt jedoch eine
deferred
Rückgabe zurück (eine Kotlin-Entität, die
Future
), sodass das Ergebnis mit
await()
abgerufen werden kann. Wenn es ohne Parameter aufgerufen wird, funktioniert es im Standardkontext für den aktuellen Bereich.
Auch hier bleibt der Haupt-Thread frei, während wir auf unsere 2 Werte warten.
Wie Sie sehen können, gibt die Startfunktion
Job
, mit dem gewartet werden kann, bis der Vorgang abgeschlossen ist. Dies erfolgt über die Funktion
join()
. Es funktioniert wie in jeder anderen Sprache, mit der Einschränkung, dass es einfach
die Coroutine suspendiert und den Fluss nicht blockiert .
VersandDispatching ist ein Schlüsselkonzept bei der Arbeit mit Coroutinen. Mit dieser Aktion können Sie von einem Thread zum anderen "springen".
Überlegen Sie, wie das Äquivalent für den Versand in
Main
in Java aussieht.
runOnUiThread: public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action);
Die
Handler
für Android ist ein
Handler
basierter Dispatcher. Das ist also in der Tat eine sehr geeignete Implementierung:
launch(Dispatchers.Main) { ... } vs launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { ... }
launch(Dispatchers.Main)
sendet
Runnable
an
Handler
, sodass der Code nicht sofort ausgeführt wird.
launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED)
führt seinen Lambda-Ausdruck sofort im aktuellen Thread aus.
Dispatchers.Main
stellt sicher, dass Coroutine, wenn die Arbeit wieder aufgenommen wird, an den Haupt-Thread weitergeleitet wird . Darüber hinaus wird Handler hier als native Android-Implementierung zum Senden an die Anwendungsereignisschleife verwendet.
Die genaue Implementierung sieht folgendermaßen aus:
val Main: HandlerDispatcher = HandlerContext(mainHandler, "Main")
Hier ist ein guter Artikel, der Ihnen hilft, die Feinheiten des Versands in Android zu verstehen:
Grundlegendes zu Android Core: Looper, Handler und HandlerThread .
Coroutine-KontextDer Coroutine-Kontext (auch als Coroutine-Manager bezeichnet) bestimmt, in welchem Thread sein Code ausgeführt wird, was zu tun ist, wenn eine Ausnahme ausgelöst wird, und verweist auf den übergeordneten Kontext, um die Stornierung zu verbreiten.
val job = Job() val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> whatever(throwable) } launch(Disaptchers.Default+exceptionHandler+job) { ... }
job.cancel()
alle Coroutinen ab, deren übergeordnetes
job.cancel()
job
. Ein Ausnahmehandler erhält alle Ausnahmen, die in diesen Coroutinen ausgelöst werden.
GeltungsbereichDie
coroutineScope
Schnittstelle vereinfacht die Fehlerbehandlung:
Wenn eine der Tochter-Coroutinen ausfällt, werden auch der gesamte Bereich und alle untergeordneten Coroutinen storniert.
Wenn es im
async
Beispiel nicht möglich war, den Wert zu extrahieren, während eine andere Aufgabe weiter funktionierte, haben wir einen beschädigten Zustand, und damit muss etwas getan werden.
Bei der Arbeit mit
useValues
wird die Funktion
useValues
nur aufgerufen, wenn beide Werte erfolgreich extrahiert wurden. Wenn
deferred2
fehlschlägt, wird
deferred1
abgebrochen.
coroutineScope { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) }
Sie können auch eine ganze Klasse in den Bereich
CoroutineContext
, um einen Standard-
CoroutineContext
dafür
CoroutineContext
und zu verwenden.
Eine Beispielklasse, die die
CoroutineScope
Schnittstelle implementiert:
open class ScopedViewModel : ViewModel(), CoroutineScope { protected val job = Job() override val coroutineContext = Dispatchers.Main+job override fun onCleared() { super.onCleared() job.cancel() } }
Ausführen von Corutin in
CoroutineScope
:
Der Standardstart- oder
async
Manager wird jetzt zum aktuellen Bereichsmanager.
launch { val foo = withContext(Dispatchers.IO) { … }
Autonomer Start von Coroutine (außerhalb eines CoroutineScope):
GlobalScope.launch(Dispatchers.Main) {
Sie können den Bereich für eine Anwendung sogar definieren, indem Sie den Standard-
Main
festlegen:
object AppScope : CoroutineScope by GlobalScope { override val coroutineContext = Dispatchers.Main.immediate }
Bemerkungen- Coroutinen beschränken die Interoperabilität mit Java
- Begrenzen Sie die Veränderlichkeit, um Sperren zu vermeiden
- Coroutinen sollen warten, nicht Threads organisieren
- Vermeiden Sie E / A in
Dispatchers.Default
(und Main
...) - dafür ist Dispatchers.IO gedacht - Streams sind ressourcenintensiv, daher werden Single-Threaded-Kontexte verwendet
Dispatchers.Default
basiert auf ForkJoinPool
, das in Android 5+ eingeführt wurde- Coroutinen können über Kanäle verwendet werden
Sperren und Rückrufe über Kanäle beseitigenKanaldefinition aus der JetBrains-Dokumentation:
Kanal Channel
BlockingQueue
konzeptionell sehr ähnlich. Der Hauptunterschied besteht darin, dass der Put-Vorgang nicht blockiert wird, ein Suspend- send
(oder ein nicht blockierendes offer
) vorgesehen ist und statt des Take-Vorgangs ein Suspend- receive
.
SchauspielerStellen Sie sich ein einfaches Werkzeug für die Arbeit mit Kanälen vor:
Actor
.
Actor
ist wiederum
Handler
sehr ähnlich: Wir definieren den Kontext der Coroutine (dh den Thread, in dem wir Aktionen ausführen werden) und arbeiten in einer sequentiellen Reihenfolge damit.
Der Unterschied besteht natürlich darin, dass hier Corutine verwendet werden;
Sie können die Leistung und die ausgeführte Codepause angeben .
Im Prinzip leitet der
actor
jeden Befehl an den Coroutine-Kanal weiter. Es
garantiert die Ausführung eines Befehls und schränkt Operationen in seinem Kontext ein . Dieser Ansatz hilft perfekt dabei,
synchronize
Anrufe loszuwerden und alle Threads frei zu halten!
protected val updateActor by lazy { actor<Update>(capacity = Channel.UNLIMITED) { for (update in channel) when (update) { Refresh -> updateList() is Filter -> filter.filter(update.query) is MediaUpdate -> updateItems(update.mediaList as List<T>) is MediaAddition -> addMedia(update.media as T) is MediaListAddition -> addMedia(update.mediaList as List<T>) is MediaRemoval -> removeMedia(update.media as T) } } }
In diesem Beispiel verwenden wir die versiegelten Kotlin-Klassen und wählen aus, welche Aktion ausgeführt werden soll.
sealed class Update object Refresh : Update() class Filter(val query: String?) : Update() class MediaAddition(val media: Media) : Update()
Darüber hinaus werden alle diese Aktionen in die Warteschlange gestellt und niemals parallel ausgeführt. Dies ist ein bequemer Weg, um
Variabilitätsgrenzen zu erreichen.
Android Lebenszyklus + CoroutinenAkteure können auch sehr nützlich sein, um die Android-Benutzeroberfläche zu steuern, das Abbrechen von Aufgaben zu vereinfachen und eine Überlastung des Hauptthreads zu verhindern.
Lassen Sie uns dies implementieren und
job.cancel()
aufrufen, wenn die Aktivität zerstört wird.
class MyActivity : AppCompatActivity(), CoroutineScope { protected val job = SupervisorJob()
Die
SupervisorJob
Klasse ähnelt dem regulären
Job
mit der einzigen Ausnahme, dass sich die Stornierung nur in der Downstream-Richtung erstreckt.
Daher brechen wir nicht alle Coroutinen in einer
Activity
wenn eine davon fehlschlägt.
Mit
einer Erweiterungsfunktion , mit der Sie von jeder
View
in
CoroutineScope
aus auf diesen
CoroutineContext
zugreifen können,
CoroutineContext
es etwas besser aus.
val View.coroutineContext: CoroutineContext? get() = (context as? CoroutineScope)?.coroutineContext
Jetzt können wir all dies
setOnClick
Funktion
setOnClick
erstellt einen kombinierten Akteur, um seine
onClick
Aktionen zu steuern. Bei mehreren Abgriffen werden Zwischenaktionen ignoriert, wodurch ANR-Fehler beseitigt werden (die Anwendung reagiert nicht), und diese Aktionen werden im Rahmen der
Activity
. Wenn die Aktivität zerstört wird, wird dies alles abgebrochen.
fun View.setOnClick(action: suspend () -> Unit) {
In diesem Beispiel setzen wir den
Channel
auf "
Conflated
, damit einige Ereignisse ignoriert werden, wenn zu viele vorhanden sind. Sie können es durch
Channel.UNLIMITED
ersetzen, wenn Sie Ereignisse lieber in die Warteschlange stellen möchten, ohne eines davon zu verlieren, die Anwendung jedoch vor ANR-Fehlern schützen möchten.
Sie können auch die Coroutinen- und Lifecycle-Frameworks kombinieren, um das Abbrechen von Aufgaben im Zusammenhang mit der Benutzeroberfläche zu automatisieren:
val LifecycleOwner.untilDestroy: Job get() { val job = Job() lifecycle.addObserver(object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }) return job }
Vereinfachen Sie die Situation mit Rückrufen (Teil 1)Hier erfahren Sie, wie Sie die Verwendung von Callback-basierten APIs mit
Channel
transformieren.
Die API funktioniert folgendermaßen:
requestBrowsing(url, listener)
analysiert den Ordner unter url
.- Der
listener
empfängt onMediaAdded(media: Media)
für jede in diesem Ordner gefundene Mediendatei. listener.onBrowseEnd()
wird beim Parsen des Ordners aufgerufen
Hier ist die alte
refresh
im Inhaltsanbieter für den VLC-Browser:
private val refreshList = mutableListOf<Media>() fun refresh() = requestBrowsing(url, refreshListener) private val refreshListener = object : EventListener{ override fun onMediaAdded(media: Media) { refreshList.add(media)) } override fun onBrowseEnd() { val list = refreshList.toMutableList() refreshList.clear() launch { dataset.value = list parseSubDirectories() } } }
Wie kann man es verbessern?Erstellen Sie einen Kanal, der in der
refresh
. Jetzt leiten Browser-Rückrufe die Medien nur noch auf diesen Kanal und schließen ihn dann.
Jetzt ist die
refresh
klarer geworden. Sie erstellt einen Kanal, ruft den VLC-Browser auf, erstellt eine Liste mit Mediendateien und verarbeitet diese.
Anstelle der
consumeEach
oder
consumeEach
Sie
for
das Warten auf das Medium verwenden. Diese Schleife wird
browserChannel
sobald der
browserChannel
geschlossen wird.
private lateinit var browserChannel : Channel<Media> override fun onMediaAdded(media: Media) { browserChannel.offer(media) } override fun onBrowseEnd() { browserChannel.close() } suspend fun refresh() { browserChannel = Channel(Channel.UNLIMITED) val refreshList = mutableListOf<Media>() requestBrowsing(url)
Vereinfachung der Situation durch Rückrufe (Teil 2): NachrüstungDer zweite Ansatz: Wir verwenden überhaupt keine Kotlinx-Coroutinen, sondern ein Coroutine-Kern-Framework.
Sehen Sie, wie Coroutinen tatsächlich funktionieren!
Die Funktion
retrofitSuspendCall
umschließt eine
Retrofit Call
Anforderung, um sie zu einer
suspend
Funktion zu machen.
Mit
suspendCoroutine
rufen wir die
Call.enqueue
Methode auf und halten die Coroutine an. Der auf diese Weise bereitgestellte Rückruf ruft
continuation.resume(response)
auf, um die Coroutine mit einer Antwort vom Server fortzusetzen, sobald sie empfangen wird.
Als nächstes müssen wir nur unsere Retrofit-Funktionen in
retrofitSuspendCall
kombinieren, um Abfrageergebnisse mit ihnen zurückzugeben.
suspend inline fun <reified T> retrofitSuspendCall(request: () -> Call <T> ) : Response <T> = suspendCoroutine { continuation -> request.invoke().enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { continuation.resume(response) } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) } suspend fun browse(path: String?) = retrofitSuspendCall { ApiClient.browse(path) }
Somit erfolgt der Anruf, der das Netzwerk blockiert, im dedizierten Retrofit-Thread, die Coroutine ist hier und wartet auf eine Antwort vom Server, und es gibt keinen Ort, an dem sie in der Anwendung verwendet werden kann!
Diese Implementierung ist von der
Gildor / Kotlin-Coroutines-Retrofit-Bibliothek inspiriert.
Es gibt auch einen
JakeWharton / retrofit2-kotlin-coroutines-Adapter mit einer anderen Implementierung, die ein ähnliches Ergebnis
liefert .
NachwortChannel
kann auf viele andere Arten verwendet werden. In
BroadcastChannel finden Sie leistungsfähigere Implementierungen, die Sie möglicherweise nützlich finden.
Sie können Kanäle auch mit der Funktion
Produzieren erstellen.
Schließlich ist es unter Verwendung von Kanälen bequem, die Kommunikation zwischen den Komponenten der Benutzeroberfläche zu organisieren: Der Adapter kann Klickereignisse über
Channel
oder beispielsweise über
Actor
an sein Fragment / seine Aktivität übertragen.