Tâches asynchrones avec des threads Java

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
    );
    ...
}