Toutes les applications Android utilisent un thread principal pour gérer les opérations de l'interface utilisateur. Appels de longue durée opérations de ce thread principal peut entraîner des blocages et un manque de réponse. Pour Par exemple, si votre application envoie une requête réseau à partir du thread principal, l'UI de votre application est figée jusqu'à ce qu'elle reçoive la réponse du réseau. Si vous utilisez Java, vous pouvez créer des threads d'arrière-plan supplémentaires pour gérer les opérations de longue durée ; le thread principal continue de gérer les mises à jour de l'UI.
Ce guide explique comment les développeurs utilisant le langage de programmation Java peuvent utiliser un pool de threads pour configurer et utiliser plusieurs threads dans une application Android. Ce guide vous montre également comment définir le code à exécuter sur un thread et comment communiquer entre l'un de ces threads et le thread principal.
Bibliothèques de simultanéité
Il est important de comprendre les bases des threads et leurs ces mécanismes. Il existe cependant de nombreuses bibliothèques populaires proposant des abstractions sur ces concepts et des utilitaires prêts à l'emploi pour transmettre des données entre les threads. Ces bibliothèques incluent Guava et RxJava pour les utilisateurs du langage de programmation Java et les coroutines que nous recommandons aux utilisateurs de Kotlin.
En pratique, vous devez choisir celle qui convient le mieux à votre application de développement, même si les règles des threads restent les mêmes.
Présentation des exemples
Les exemples présentés dans cette rubrique sont basés sur le Guide de l'architecture des applications. requête réseau et renvoie le résultat au thread principal, où l'application peut afficher ce résultat à l'écran.
Plus précisément, ViewModel
appelle la couche de données sur le thread principal pour
la requête réseau. La couche de données est chargée de déplacer
l'exécution de la requête réseau en dehors du thread principal et la publication du résultat
au thread principal à l'aide d'un rappel.
Pour déplacer l'exécution de la requête réseau en dehors du thread principal, nous devons créer d'autres threads dans notre application.
Créer plusieurs threads
Un pool de threads est une collection gérée de threads qui exécute des tâches dans
parallèlement à une file d'attente. Les nouvelles tâches sont exécutées sur des threads existants
les threads deviennent inactifs. Pour envoyer une tâche à un pool de threads, utilisez la
ExecutorService
. Notez que ExecutorService
n'a rien à voir
avec Services, le composant d'application Android.
La création de threads est coûteuse. Vous ne devez donc créer un pool de threads qu'une seule fois
que votre application initialise. Veillez à enregistrer l'instance de ExecutorService
.
soit dans la classe Application
, soit dans un conteneur d'injection de dépendances.
L'exemple suivant crée un pool de quatre threads que nous pouvons utiliser pour
exécuter des tâches en arrière-plan.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Il existe d'autres façons de configurer un pool de threads en fonction des charge de travail spécifique. Pour en savoir plus, consultez la section Configurer un pool de threads.
Exécuter dans un thread d'arrière-plan
Effectuer une requête réseau sur le thread principal entraîne l'attente du thread.
block, jusqu'à ce qu'il reçoive une réponse. Comme le thread est bloqué, le système d'exploitation ne peut pas
appelez onDraw()
, et votre application se fige, ce qui peut entraîner une erreur
Boîte de dialogue "Répondre" (ANR). Exécutons plutôt cette opération sur un arrière-plan
thread.
Envoyer la demande
Tout d'abord, examinons la classe LoginRepository
et voyons comment elle contribue
la requête réseau:
// 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()
est synchrone et bloque le thread d'appel. Pour modéliser les
à la requête réseau, nous avons notre propre classe Result
.
Déclencher la requête
ViewModel
déclenche la requête réseau lorsque l'utilisateur appuie, par exemple sur
un bouton:
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);
}
}
Avec le code précédent, LoginViewModel
bloque le thread principal lors de la création
la requête réseau. Nous pouvons utiliser le pool de threads que nous avons instancié pour déplacer
l'exécution sur un thread d'arrière-plan.
Gérer l'injection de dépendances
Tout d'abord, en suivant les principes de l'injection de dépendances, LoginRepository
utilise une instance d'Executor et non de ExecutorService
, car
exécuter du code et ne pas gérer les threads:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
La méthode execute() de l'exécuteur utilise un Runnable. Un Runnable
est un
Interface SAM (Single Abstract Method) avec une méthode run()
exécutée dans
un thread lorsqu'il est appelé.
Exécuter en arrière-plan
Créons une autre fonction appelée makeLoginRequest()
, qui déplace
sur le thread d'arrière-plan et ignore la réponse pour le moment:
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
}
...
}
Dans la méthode execute()
, nous créons un Runnable
avec le bloc de code.
que nous voulons exécuter dans le thread d'arrière-plan. Dans notre cas, le réseau synchrone
. En interne, ExecutorService
gère les Runnable
et
l'exécute dans un thread disponible.
Points à prendre en compte
Tout thread de votre application peut s'exécuter en parallèle d'autres threads, y compris le thread principal Vous devez donc vous assurer que votre code est thread-safe. Notez que dans notre exemple qu'on évite d'écrire dans des variables partagées entre les threads, de transmettre des données immuables à la place. C'est une bonne pratique, car chaque fil de discussion fonctionne avec ses propres instances de données, et la synchronisation est simplifiée.
Si vous devez partager l'état entre les threads, vous devez veiller à gérer l'accès à partir de threads à l'aide de mécanismes de synchronisation tels que les verrous. Ceci est en dehors de le champ d'application de ce guide. En général, vous devez éviter de partager un état modifiable entre les threads autant que possible.
Communiquer avec le thread principal
À l'étape précédente, nous avons ignoré la réponse à la requête réseau. Pour afficher la
résultat à l'écran, LoginViewModel
doit en être informé. Nous pouvons le faire en
à l'aide de rappels.
La fonction makeLoginRequest()
doit utiliser un rappel en tant que paramètre afin que :
il peut renvoyer une valeur de manière asynchrone. Le rappel avec le résultat est appelé
chaque fois que la requête réseau
se termine ou qu'un échec se produit. En Kotlin, nous pouvons
utiliser une fonction
d'ordre supérieur. Cependant, en Java, nous devons créer un nouveau rappel
aient les mêmes fonctionnalités:
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
doit implémenter le rappel maintenant. Il peut effectuer différentes opérations
en fonction du résultat:
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
}
}
});
}
}
Dans cet exemple, le rappel est exécuté dans le thread appelant, qui est un thread d'arrière-plan. Cela signifie que vous ne pouvez pas modifier ni communiquer directement avec la couche d'UI jusqu'à ce que vous reveniez au thread principal.
Utiliser des gestionnaires
Vous pouvez utiliser un Handler pour mettre en file d'attente une action à effectuer sur un autre
thread. Pour spécifier le thread sur lequel exécuter l'action, créez le
Handler
à l'aide d'un looper pour le thread. Un Looper
est un objet qui s'exécute
dans la boucle de messages d'un fil de discussion associé. Une fois que vous avez créé un Handler
, vous
vous pouvez ensuite utiliser la méthode post(Runnable) pour exécuter un bloc de code dans
thread correspondant.
Looper
inclut une fonction d'assistance, getMainLooper(), qui récupère les
Looper
du thread principal. Vous pouvez exécuter du code dans le thread principal à l'aide de cette
Looper
pour créer un Handler
Comme c'est quelque chose que
vous pourriez faire assez souvent,
vous pouvez également enregistrer une instance de Handler
à l'endroit où vous avez enregistré le
ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Il est recommandé d'injecter le gestionnaire dans le dépôt, car il donne
plus de flexibilité. Par exemple, à l'avenir, vous voudrez peut-être transmettre un
un Handler
différent pour planifier des tâches dans un thread distinct. Si vous avez toujours
communiquant avec le même thread, vous pouvez transmettre le Handler
au
de dépôt, comme illustré dans l'exemple suivant.
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);
}
});
}
...
}
Pour plus de flexibilité, vous pouvez également transmettre un Handler
à chaque
:
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);
}
});
}
}
Dans cet exemple, le rappel transmis dans l'objet makeLoginRequest
du Repository
est exécuté sur le thread principal. Vous pouvez donc modifier directement l'UI
à partir du rappel ou utiliser LiveData.setValue()
pour communiquer avec l'UI.
Configurer un pool de threads
Vous pouvez créer un pool de threads à l'aide de l'une des fonctions d'assistance Executor avec des paramètres prédéfinis, comme indiqué dans l'exemple de code précédent. Par ailleurs, Si vous souhaitez personnaliser les détails du pool de threads, vous pouvez créer un à l'aide de ThreadPoolExecutor. Vous pouvez configurer les paramètres suivants : détails:
- Taille initiale et maximale du pool.
- Maintenir la durée de vie et l'unité de temps. La durée de conservation correspond à la durée maximale qu'un thread peut rester inactif avant son arrêt.
- File d'attente d'entrée contenant
Runnable
tâches. Cette file d'attente doit implémenter BlockingQueue. Pour répondre aux exigences de votre application, vous pouvez choisir parmi les implémentations de file d'attente disponibles. Pour en savoir plus, consultez le cours présentation de ThreadPoolExecutor.
Voici un exemple qui spécifie la taille du pool de threads en fonction du nombre total de cœurs de processeur, un temps de conservation d'une seconde et une file d'attente d'entrée.
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
);
...
}