Alle Android-Apps verwenden einen Hauptthread zur Verarbeitung von UI-Vorgängen. Anruf mit langer Ausführungszeit Vorgänge von diesem Hauptthread können zu einem Einfrieren und Nichtreagieren führen. Für Wenn Ihre Anwendung beispielsweise eine Netzwerkanfrage aus dem Hauptthread sendet, wird die Benutzeroberfläche der Anwendung ist eingefroren, bis die Netzwerkantwort eingeht. Wenn Sie Java verwenden, können Sie zusätzliche Hintergrundthreads erstellen, um lang andauernde Vorgänge verarbeitet der Hauptthread weiterhin UI-Aktualisierungen.
In diesem Leitfaden erfahren Sie, wie Entwickler, die die Programmiersprache Java verwenden, Thread-Pool, um mehrere Threads in einer Android-App einzurichten und zu verwenden. Dieser Leitfaden zeigt Ihnen auch, wie Sie Code zur Ausführung in einem Thread definieren und wie Sie zwischen einem dieser Threads und dem Hauptthread.
Gleichzeitigkeitsbibliotheken
Es ist wichtig, die Grundlagen von Threading Mechanismen. Es gibt jedoch viele beliebte Bibliotheken, die Konzepte und einsatzbereite Dienstprogramme zur Datenweitergabe zwischen Threads zu wechseln. Zu diesen Bibliotheken gehören Guava und RxJava für Nutzer der Java-Programmiersprache und Coroutines, die wir Kotlin-Nutzern empfehlen.
In der Praxis sollten Sie sich für die Methode entscheiden, die für Ihre App und Ihren Entwicklungsteam, obwohl die Regeln des Threading dieselben bleiben.
Beispiele – Übersicht
Die Beispiele in diesem Thema basieren auf dem Leitfaden zur App-Architektur. Netzwerkanfrage senden und das Ergebnis an den Hauptthread zurückgeben. wird dieses Ergebnis möglicherweise auf dem Bildschirm angezeigt.
Insbesondere ruft ViewModel
die Datenschicht im Hauptthread auf, um
die Netzwerkanfrage auslösen. Über die Datenschicht werden die Daten
Ausführung der Netzwerkanfrage aus dem Hauptthread und Veröffentlichen des Ergebnisses
über einen Callback mit dem Hauptthread verbunden.
Um die Ausführung der Netzwerkanfrage aus dem Hauptthread zu verschieben, müssen wir andere Threads in unserer App zu erstellen.
Mehrere Threads erstellen
Ein Thread-Pool ist eine verwaltete Sammlung von Threads, in denen Aufgaben ausgeführt werden.
parallel zu einer Warteschlange. Neue Aufgaben werden auf vorhandenen Threads ausgeführt,
und Threads werden inaktiv. Verwenden Sie zum Senden einer Aufgabe an einen Thread-Pool die Methode
ExecutorService
-Schnittstelle. ExecutorService
hat nichts zu tun
mit Services, der Android-Anwendungskomponente.
Das Erstellen von Threads ist teuer, daher sollten Sie einen Thread-Pool nur einmal erstellen,
die App initialisiert wird. Denken Sie daran, die Instanz von ExecutorService
zu speichern.
entweder in Ihrer Application
-Klasse oder in einem Abhängigkeitsinjektions-Container.
Im folgenden Beispiel wird ein Thread-Pool aus vier Threads erstellt, mit dem wir
Hintergrundaufgaben ausführen.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Es gibt andere Möglichkeiten, einen Threadpool zu konfigurieren, je nach erwartetem Arbeitsbelastung. Weitere Informationen finden Sie unter Thread-Pool konfigurieren.
In einem Hintergrundthread ausführen
Eine Netzwerkanfrage an den Hauptthread führt dazu, dass der Thread wartet.
block, bis eine Antwort eingeht. Da der Thread blockiert ist, kann das Betriebssystem
onDraw()
aufrufen und Ihre App bleibt hängen. Dies kann zu einer nicht
Dialogfeld „Antwort (ANR)“. Führen wir diesen Vorgang stattdessen im Hintergrund aus.
Diskussions-Thread.
Anfrage stellen
Sehen wir uns zuerst unseren LoginRepository
-Kurs an.
Netzwerkanfrage:
// Result.java
public abstract class Result<T> {
private Result() {}
public static final class Success<T> extends Result<T> {
public T data;
public Success(T data) {
this.data = data;
}
}
public static final class Error<T> extends Result<T> {
public Exception exception;
public Error(Exception exception) {
this.exception = exception;
}
}
}
// LoginRepository.java
public class LoginRepository {
private final String loginUrl = "https://example.com/login";
private final LoginResponseParser responseParser;
public LoginRepository(LoginResponseParser responseParser) {
this.responseParser = responseParser;
}
public Result<LoginResponse> makeLoginRequest(String jsonBody) {
try {
URL url = new URL(loginUrl);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestMethod("POST");
httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
httpConnection.setRequestProperty("Accept", "application/json");
httpConnection.setDoOutput(true);
httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));
LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
return new Result.Success<LoginResponse>(loginResponse);
} catch (Exception e) {
return new Result.Error<LoginResponse>(e);
}
}
}
makeLoginRequest()
ist synchron und blockiert den aufrufenden Thread. Um die
Antwort der Netzwerkanfrage haben wir unsere eigene Result
-Klasse.
Anfrage auslösen
Der ViewModel
löst die Netzwerkanfrage aus, wenn der Nutzer beispielsweise auf
eine Schaltfläche:
public class LoginViewModel {
private final LoginRepository loginRepository;
public LoginViewModel(LoginRepository loginRepository) {
this.loginRepository = loginRepository;
}
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody);
}
}
Mit dem vorherigen Code blockiert LoginViewModel
den Hauptthread beim Erstellen
der Netzwerkanfrage. Wir können den Thread-Pool, den wir instanziiert haben, zum Verschieben
die Ausführung in einem Hintergrundthread.
Abhängigkeitsinjektion handhaben
Zuerst anhand der Prinzipien der Abhängigkeitsinjektion LoginRepository
nimmt eine Instanz von Executor und nicht ExecutorService
,
Code ausführen und keine Threads verwalten:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
Die Methode execute() des Executors verwendet ein Runnable. Ein Runnable
ist eine
Schnittstelle für eine einzelne abstrakte Methode (Single Abstrakte Methode) mit einer run()
-Methode, die ausgeführt wird in
einen Thread aufrufen.
Im Hintergrund ausführen
Erstellen wir nun eine weitere Funktion namens makeLoginRequest()
,
im Hintergrundthread ausgeführt und die Antwort vorerst ignoriert:
public class LoginRepository {
...
public void makeLoginRequest(final String jsonBody) {
executor.execute(new Runnable() {
@Override
public void run() {
Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
}
});
}
public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
... // HttpURLConnection logic
}
...
}
Innerhalb der Methode execute()
erstellen wir eine neue Runnable
mit dem Codeblock
möchten wir im Hintergrund-Thread
ausführen, in unserem Fall im synchronen
-Anforderungsmethode festlegen. Intern verwaltet die ExecutorService
die Runnable
und
in einem verfügbaren Thread
ausgeführt wird.
Wissenswertes
Jeder Thread in Ihrer Anwendung kann parallel zu anderen Threads ausgeführt werden, einschließlich des Hauptthreads -Thread, Sie sollten also darauf achten, dass Ihr Code threadsicher ist. Beachten Sie, dass in der dass wir nicht in Variablen schreiben, die zwischen Threads gemeinsam genutzt werden, unveränderliche Daten. Dies ist eine bewährte Methode, da jeder Thread mit eine eigene Instanz von Daten erstellen und die Komplexität der Synchronisierung vermeiden.
Wenn Sie den Status zwischen Threads teilen müssen, müssen Sie bei der Zugriffsverwaltung vorsichtig sein aus Threads mithilfe von Synchronisierungsmechanismen wie Sperren. Dies ist außerhalb von den Umfang dieses Leitfadens. Im Allgemeinen sollten Sie die Freigabe veränderlicher Status vermeiden wenn möglich zwischen Threads wechseln.
Mit dem Hauptthread kommunizieren
Im vorherigen Schritt wurde die Antwort auf die Netzwerkanfrage ignoriert. Zum Anzeigen der
Ergebnis auf dem Bildschirm angezeigt wird, muss LoginViewModel
darüber informiert werden. Das können wir tun, indem wir
mithilfe von Callbacks.
Die Funktion makeLoginRequest()
sollte einen Callback als Parameter annehmen, sodass
kann er einen Wert asynchron zurückgeben. Der Callback mit dem Ergebnis wird
wenn die Netzwerkanfrage abgeschlossen wird oder ein Fehler auftritt. In Kotlin können wir
eine übergeordnete Funktion. In Java müssen wir jedoch einen neuen Callback erstellen,
mit der gleichen Funktionalität:
interface RepositoryCallback<T> {
void onComplete(Result<T> result);
}
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
callback.onComplete(result);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
callback.onComplete(errorResult);
}
}
});
}
...
}
ViewModel
muss den Callback jetzt implementieren. Sie kann zu unterschiedlichen
Logik, abhängig vom Ergebnis:
public class LoginViewModel {
...
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
@Override
public void onComplete(Result<LoginResponse> result) {
if (result instanceof Result.Success) {
// Happy path
} else {
// Show error in UI
}
}
});
}
}
In diesem Beispiel wird der Callback im aufrufenden Thread ausgeführt, der ein im Hintergrund. Das bedeutet, dass Sie keine Änderungen vornehmen oder direkt kommunizieren können. mit dem UI-Layer, bis Sie wieder zum Hauptthread wechseln.
Handler verwenden
Sie können einen Handler verwenden, um eine Aktion in die Warteschlange zu stellen, die an einem anderen Ort ausgeführt werden soll.
Diskussions-Thread. Um den Thread anzugeben, auf dem die Aktion ausgeführt werden soll, erstellen Sie die
Handler
unter Verwendung eines Loopers für den Thread. Looper
ist ein Objekt, das ausgeführt wird,
die Nachrichtenschleife für einen verknüpften Thread. Nachdem Sie ein Handler
erstellt haben,
können Sie dann mit der Methode post(Runnable) einen Codeblock im
zum entsprechenden Thread.
Looper
enthält die Hilfsfunktion getMainLooper(), mit der die
Looper
des Hauptthreads. Mit diesem Befehl können Sie Code im Hauptthread ausführen:
Looper
, um ein Handler
zu erstellen. Da dies oft vorkommt,
können Sie auch eine Handler
-Instanz an dem Ort speichern, an dem Sie die Datei gespeichert haben.
ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Es empfiehlt sich, den Handler in das Repository einzuschleusen, da dadurch
mehr Flexibilität. Vielleicht möchten Sie in Zukunft zum Beispiel
verschiedene Handler
, um Aufgaben in einem separaten Thread zu planen. Wenn Sie immer
wieder mit demselben Thread kommunizieren, können Sie das Handler
an den
Repository-Konstruktor wie im folgenden Beispiel gezeigt.
public class LoginRepository {
...
private final Handler resultHandler;
public LoginRepository(LoginResponseParser responseParser, Executor executor,
Handler resultHandler) {
this.responseParser = responseParser;
this.executor = executor;
this.resultHandler = resultHandler;
}
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
...
}
Wenn Sie mehr Flexibilität wünschen, können Sie auch ein Handler
an jedes
:
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler,
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback, resultHandler);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback, resultHandler);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
}
In diesem Beispiel wurde der Callback an den makeLoginRequest
des Repositorys übergeben.
-Aufruf im Hauptthread ausgeführt wird. Das heißt, Sie können die Benutzeroberfläche direkt
aus dem Callback aus oder verwende LiveData.setValue()
, um mit der UI zu kommunizieren.
Threadpool konfigurieren
Sie können einen Threadpool mit einer der Hilfsfunktionen von Executor erstellen. mit vordefinierten Einstellungen, wie im vorherigen Beispielcode gezeigt. Alternativ können Sie Wenn Sie die Details des Thread-Pools anpassen möchten, können Sie einen mit ThreadPoolExecutor direkt aufrufen. Sie können Folgendes konfigurieren: Details:
- Anfängliche und maximale Poolgröße.
- Alive-Zeit und Zeiteinheit beibehalten. Die Keep-Alive-Zeit ist die maximale Dauer, kann der Thread inaktiv bleiben, bevor er heruntergefahren wird.
- Eine Eingabewarteschlange, die
Runnable
Aufgaben enthält. Diese Warteschlange muss den Parameter BlockingQueue. So passen Sie die Anforderungen Ihrer App an: eine der verfügbaren Warteschlangenimplementierungen auswählen. Weitere Informationen findest du im Kurs Übersicht für ThreadPoolExecutor
Hier ist ein Beispiel, bei dem die Threadpoolgröße basierend auf der Gesamtzahl der Prozessorkerne, eine Keep-Alive-Zeit von einer Sekunde und eine Eingabewarteschlange.
public class MyApplication extends Application {
/*
* Gets the number of available cores
* (not always the same as the maximum number of cores)
*/
private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
// Instantiates the queue of Runnables as a LinkedBlockingQueue
private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
// Sets the amount of time an idle thread waits before terminating
private static final int KEEP_ALIVE_TIME = 1;
// Sets the Time Unit to seconds
private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
// Creates a thread pool manager
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
NUMBER_OF_CORES, // Initial pool size
NUMBER_OF_CORES, // Max pool size
KEEP_ALIVE_TIME,
KEEP_ALIVE_TIME_UNIT,
workQueue
);
...
}