Alle Android-Apps verwenden einen Hauptthread zur Verarbeitung von UI-Vorgängen. Wenn Vorgänge mit langer Ausführungszeit von diesem Hauptthread aus aufgerufen werden, kann es zu Einfrieren und Nichtreaktion kommen. Wenn Ihre Anwendung beispielsweise eine Netzwerkanfrage vom Hauptthread aus sendet, bleibt die Benutzeroberfläche Ihrer Anwendung eingefroren, bis sie die Netzwerkantwort erhält. Wenn Sie Java verwenden, können Sie zusätzliche Hintergrundthreads erstellen, um lang andauernde Vorgänge zu verarbeiten, während der Hauptthread weiterhin UI-Aktualisierungen verarbeitet.
In diesem Leitfaden erfahren Sie, wie Entwickler, die die Programmiersprache Java verwenden, einen Thread-Pool verwenden können, um in einer Android-App mehrere Threads einzurichten und zu verwenden. Außerdem erfahren Sie, wie Sie Code definieren, der in einem Thread ausgeführt wird, und wie zwischen einem dieser Threads und dem Hauptthread kommuniziert wird.
Nebenläufigkeitsbibliotheken
Es ist wichtig, die Grundlagen von Threading und die zugrunde liegenden Mechanismen zu verstehen. Es gibt jedoch viele beliebte Bibliotheken, die eine höhere Abstraktion dieser Konzepte und gebrauchsfertige Dienstprogramme für die Weitergabe von Daten zwischen Threads ermöglichen. Zu diesen Bibliotheken gehören Guava und RxJava für Nutzer der Programmiersprache Java sowie Coroutinen, die wir Nutzern von Kotlin empfehlen.
In der Praxis sollten Sie diejenige auswählen, die für Ihre Anwendung und Ihr Entwicklungsteam am besten geeignet ist, wobei die Regeln für das Threading unverändert bleiben.
Beispiele – Übersicht
Basierend auf dem Leitfaden zur Anwendungsarchitektur wird bei den Beispielen in diesem Thema eine Netzwerkanfrage gestellt und das Ergebnis an den Hauptthread zurückgegeben. Dort kann die Anwendung dieses Ergebnis dann auf dem Bildschirm anzeigen.
Insbesondere ruft ViewModel
die Datenschicht im Hauptthread auf, um die Netzwerkanfrage auszulösen. Die Datenschicht ist dafür zuständig, die Ausführung der Netzwerkanfrage aus dem Hauptthread zu verschieben und das Ergebnis mithilfe eines Callbacks zurück an den Hauptthread zu senden.
Um die Ausführung der Netzwerkanfrage aus dem Hauptthread zu verschieben, müssen wir weitere Threads in der Anwendung erstellen.
Mehrere Threads erstellen
Ein Thread-Pool ist eine verwaltete Sammlung von Threads, die Aufgaben parallel aus einer Warteschlange ausführen. Neue Aufgaben werden in vorhandenen Threads ausgeführt, wenn diese inaktiv werden. Verwenden Sie die Schnittstelle ExecutorService
, um eine Aufgabe an einen Threadpool zu senden. ExecutorService
hat nichts mit Diensten zu tun, der Android-Anwendungskomponente.
Das Erstellen von Threads ist teuer. Daher sollten Sie einen Threadpool nur einmal während der Initialisierung Ihrer Anwendung erstellen. Speichern Sie die Instanz von ExecutorService
entweder in der Application
-Klasse oder in einem Abhängigkeitsinjektions-Container.
Im folgenden Beispiel wird ein Thread-Pool aus vier Threads erstellt, mit denen Hintergrundaufgaben ausgeführt werden können.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Es gibt andere Möglichkeiten, einen Thread-Pool abhängig von der erwarteten Arbeitslast zu konfigurieren. Weitere Informationen finden Sie unter Thread-Pool konfigurieren.
In einem Hintergrundthread ausführen
Bei einer Netzwerkanfrage an den Hauptthread wird der Thread gewartet oder blockiert, bis er eine Antwort erhält. Da der Thread blockiert ist, kann das Betriebssystem onDraw()
nicht aufrufen und Ihre Anwendung bleibt hängen, was zu einem Dialogfeld „App antwortet nicht“ (ANR) führt. Lassen Sie uns diesen Vorgang stattdessen in einem Hintergrundthread ausführen.
Anfrage stellen
Sehen wir uns zuerst die Klasse LoginRepository
an und sehen wir uns an, wie sie die Netzwerkanfrage sendet:
// 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 auf die Netzwerkanfrage zu modellieren, verwenden wir eine eigene Result
-Klasse.
Anfrage auslösen
Die ViewModel
löst die Netzwerkanfrage aus, wenn der Nutzer auf eine Schaltfläche tippt:
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, wenn die Netzwerkanfrage gestellt wird. Mit dem von uns instanziierten Thread-Pool
verschieben wir die Ausführung in einen Hintergrundthread.
Abhängigkeitsinjektion handhaben
Zuerst verwendet LoginRepository
gemäß den Prinzipien der Abhängigkeitsinjektion eine Instanz von Executor anstelle von ExecutorService
, da er Code ausführt und keine Threads verwaltet:
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 abstrakte Methode (Single Abstrakte Methode) mit einer run()
-Methode, die beim Aufrufen in einem Thread ausgeführt wird.
Im Hintergrund ausführen
Lassen Sie uns eine weitere Funktion namens makeLoginRequest()
erstellen, die die Ausführung in den Hintergrundthread verschiebt 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
}
...
}
In der Methode execute()
erstellen wir eine neue Runnable
mit dem Codeblock, den wir im Hintergrundthread ausführen möchten – in unserem Fall die synchrone Netzwerkanfragemethode. Intern verwaltet der ExecutorService
den Runnable
und führt ihn in einem verfügbaren Thread aus.
Wissenswertes
Jeder Thread in der Anwendung kann parallel zu anderen Threads ausgeführt werden, einschließlich des Hauptthreads. Daher sollten Sie dafür sorgen, dass Ihr Code Thread-sicher ist. Beachten Sie, dass wir in unserem Beispiel nicht in Variablen schreiben, die zwischen Threads gemeinsam genutzt werden, und übergeben stattdessen unveränderliche Daten. Dies ist eine bewährte Methode, da jeder Thread mit seiner eigenen Dateninstanz arbeitet und die Komplexität der Synchronisierung vermieden wird.
Wenn Sie den Status zwischen Threads teilen müssen, müssen Sie vorsichtig sein, um den Zugriff von Threads mithilfe von Synchronisierungsmechanismen wie Sperren zu verwalten. Dies wird in diesem Leitfaden nicht behandelt. Im Allgemeinen sollten Sie nach Möglichkeit vermeiden, einen änderbaren Status zwischen Threads zu teilen.
Mit dem Hauptthread kommunizieren
Im vorherigen Schritt haben wir die Antwort auf die Netzwerkanfrage ignoriert. Damit das Ergebnis auf dem Bildschirm angezeigt wird, muss LoginViewModel
darüber Bescheid wissen. Dazu können wir Callbacks verwenden.
Die Funktion makeLoginRequest()
sollte einen Callback als Parameter verwenden, damit ein Wert asynchron zurückgegeben werden kann. Der Callback mit dem Ergebnis wird immer dann aufgerufen, wenn die Netzwerkanfrage abgeschlossen ist oder ein Fehler auftritt. In Kotlin können wir eine
höhere Reihenfolge verwenden. In Java müssen wir jedoch eine neue Callback-Schnittstelle erstellen, um die gleiche Funktionalität zu haben:
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);
}
}
});
}
...
}
Der ViewModel
muss den Callback jetzt implementieren. Sie kann je nach Ergebnis unterschiedliche Logiken ausführen:
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, bei dem es sich um einen Hintergrundthread handelt. Das bedeutet, dass Sie die UI-Ebene erst dann ändern oder direkt mit ihr kommunizieren können, wenn Sie zurück zum Hauptthread wechseln.
Handler verwenden
Sie können einen Handler verwenden, um eine Aktion in die Warteschlange zu stellen, die für einen anderen Thread ausgeführt werden soll. Erstellen Sie das Handler
mit einem Looper für den Thread, um den Thread anzugeben, für den die Aktion ausgeführt werden soll. Ein Looper
ist ein Objekt, das die Nachrichtenschleife für einen verknüpften Thread ausführt. Nachdem Sie einen Handler
erstellt haben, können Sie mit der Methode post(Runnable) einen Codeblock im entsprechenden Thread ausführen.
Looper
enthält die Hilfsfunktion getMainLooper(), die den Looper
des Hauptthreads abruft. Sie können Code im Hauptthread ausführen. Dazu verwenden Sie Looper
, um ein Handler
zu erstellen. Da dies sehr häufig der Fall ist, können Sie auch eine Instanz von Handler
dort speichern, wo Sie ExecutorService
gespeichert haben:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Es empfiehlt sich, den Handler in das Repository zu injizieren, da Sie dadurch mehr Flexibilität haben. In Zukunft möchten Sie beispielsweise einen anderen Handler
übergeben, um Aufgaben in einem separaten Thread zu planen. Wenn Sie immer zum selben Thread zurückkehren, können Sie den Handler
an den Repository-Konstruktor übergeben, 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 alternativ eine Handler
an jede Funktion übergeben:
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 wird der an den makeLoginRequest
-Aufruf des Repositorys übergebene Callback im Hauptthread ausgeführt. Das bedeutet, dass Sie die UI direkt über den Callback ändern oder LiveData.setValue()
für die Kommunikation mit der UI verwenden können.
Threadpool konfigurieren
Sie können einen Thread-Pool mit einer der Executor-Hilfsfunktionen mit vordefinierten Einstellungen erstellen, wie im vorherigen Beispielcode gezeigt. Wenn Sie die Details des Threadpools anpassen möchten, können Sie alternativ direkt mit ThreadPoolExecutor eine Instanz erstellen. Sie können die folgenden Details konfigurieren:
- Anfängliche und maximale Poolgröße.
- Keep alive Time und Zeiteinheit. Die Keep-Alive-Zeit ist die maximale Dauer, die ein Thread inaktiv sein kann, bevor er herunterfährt.
- Eine Eingabewarteschlange, die
Runnable
Aufgaben enthält. Diese Warteschlange muss die BlockingQueue-Schnittstelle implementieren. Je nach Anforderungen Ihrer Anwendung können Sie aus den verfügbaren Warteschlangenimplementierungen auswählen. Weitere Informationen finden Sie in der Klassenübersicht für ThreadPoolExecutor.
In diesem Beispiel wird die Größe des Threadpools anhand der Gesamtzahl der Prozessorkerne, einer Keep-Alive-Zeit von einer Sekunde und einer Eingabewarteschlange angegeben.
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
);
...
}