Lavoro asincrono con i thread Java

Tutte le app per Android utilizzano un thread principale per gestire le operazioni dell'interfaccia utente. Chiamate di lunga durata operazioni da questo thread principale possono causare blocchi e mancata reattività. Per Ad esempio, se la tua app effettua una richiesta di rete dal thread principale, l'UI dell'app è bloccato finché non riceve la risposta della rete. Se utilizzi Java, puoi Creare thread in background aggiuntivi per gestire le operazioni a lunga esecuzione, il thread principale continua a gestire gli aggiornamenti della UI.

Questa guida illustra in che modo gli sviluppatori che utilizzano il linguaggio di programmazione Java possono utilizzare un pool di thread per configurare e utilizzare più thread in un'app per Android. Questa guida ti mostra anche come definire il codice da eseguire su un thread e come comunicare tra uno di questi thread e il thread principale.

Librerie di contemporaneità

È importante comprendere le basi dell'organizzazione in thread e le sue i meccanismi della ricerca di informazioni. Esistono, tuttavia, molte librerie popolari che offrono astrazioni su questi concetti e utilità pronte all'uso per il passaggio dei dati tra i thread. Queste biblioteche includono Guava e RxJava per gli utenti del linguaggio di programmazione Java e Coroutines, che consigliamo agli utenti di Kotlin.

In pratica, devi scegliere quello più adatto alla tua app e alle tue team di sviluppo, sebbene le regole dell'organizzazione in thread siano le stesse.

Panoramica degli esempi

In base alla Guida all'architettura delle app, gli esempi in questo argomento costituiscono una una richiesta di rete e restituisce il risultato al thread principale, dove l'app potrebbe visualizzare il risultato sullo schermo.

In particolare, ViewModel chiama il livello dati sul thread principale a attivare la richiesta di rete. Il livello dati si occupa di spostare esecuzione della richiesta di rete dal thread principale e pubblicazione del risultato al thread principale utilizzando un callback.

Per spostare l'esecuzione della richiesta di rete dal thread principale, dobbiamo creare altri thread nella nostra app.

Crea più thread

Un pool di thread è una raccolta gestita di thread che esegue attività in in parallelo da una coda. Vengono eseguite nuove attività sui thread esistenti mentre i thread diventano inattivi. Per inviare un'attività a un pool di thread, utilizza ExecutorService. Tieni presente che ExecutorService non ha nulla da fare con Services, il componente dell'applicazione Android.

La creazione di thread è costosa, quindi dovresti creare un pool di thread solo una volta viene inizializzata dall'app. Assicurati di salvare l'istanza di ExecutorService nella classe Application o in un container di inserimento delle dipendenze. L'esempio seguente crea un pool di quattro thread da utilizzare per eseguire attività in background.

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

Esistono altri modi per configurare un pool di thread, a seconda del tipo previsto carico di lavoro. Per ulteriori informazioni, consulta Configurazione di un pool di thread.

Esegui in un thread in background

Quando si effettua una richiesta di rete sul thread principale, il thread rimane in attesa oppure block, finché non riceve una risposta. Poiché il thread è bloccato, il sistema operativo non può chiama onDraw() e l'app si blocca, causando potenzialmente un errore Finestra di dialogo di risposta (ANR). Eseguiamo invece questa operazione in background .

Effettua la richiesta

Innanzitutto, diamo un'occhiata alla lezione LoginRepository e vediamo come sta andando la richiesta di rete:

// 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() è sincrono e blocca il thread di chiamata. Per modellare risposta della richiesta di rete, abbiamo la nostra classe Result.

Attiva la richiesta

ViewModel attiva la richiesta di rete quando l'utente tocca, ad esempio, On un pulsante:

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

Con il codice precedente, LoginViewModel blocca il thread principale quando crea la richiesta di rete. Possiamo usare il pool di thread di cui abbiamo creato l'istanza per spostare l'esecuzione in un thread in background.

Gestire l'inserimento delle dipendenze

Innanzitutto, seguendo i principi dell'inserimento delle dipendenze, LoginRepository un'istanza di Esecutore anziché ExecutorService perché è che esegue codice e non gestisce i thread:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

Il metodo execute() dell'esecutore accetta un valore Runnable. Un Runnable è un Interfaccia SAM (Single Abstract Method) con un metodo run() che viene eseguito in un thread quando viene richiamato.

Esegui in background

Creiamo un'altra funzione denominata makeLoginRequest() che sposti la sul thread in background e per il momento ignora la risposta:

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

Nel metodo execute() viene creato un nuovo Runnable con il blocco di codice vogliamo eseguire nel thread in background, nel nostro caso la rete sincrona metodo di richiesta. Internamente, l'ExecutorService gestisce Runnable e lo esegue in un thread disponibile.

Considerazioni

Qualsiasi thread nella tua app può essere eseguito in parallelo ad altri thread, incluso il thread principale quindi devi assicurarti che il codice sia protetto da thread. Nota che nelle nostre ad esempio in cui evitiamo di scrivere in variabili condivise tra thread, immutabili. Questa è una buona prassi, perché ogni thread funziona con la propria istanza di dati, evitando la complessità della sincronizzazione.

Se devi condividere lo stato tra i thread, devi prestare attenzione alla gestione dell'accesso dai thread utilizzando meccanismi di sincronizzazione come i blocchi. Questo è al di fuori di nell'ambito di questa guida. In generale, dovresti evitare di condividere lo stato modificabile tra i thread quando possibile.

Comunicare con il thread principale

Nel passaggio precedente abbiamo ignorato la risposta alla richiesta di rete. Per visualizzare risultato sullo schermo, LoginViewModel deve esserne a conoscenza. Possiamo farlo mediante i callback.

La funzione makeLoginRequest() deve prendere un callback come parametro in modo che può restituire un valore in modo asincrono. Il callback con il risultato viene chiamato ogni volta che la richiesta di rete viene completata o si verifica un errore. In Kotlin, possiamo utilizzare una funzione di ordine superiore. In Java, invece, dobbiamo creare un nuovo callback avere la stessa funzionalità:

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 deve implementare il callback ora. Può avere prestazioni diverse a seconda del risultato:

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 questo esempio, il callback viene eseguito nel thread di chiamata, ovvero in background. Ciò significa che non puoi modificare o comunicare direttamente con il livello UI finché non torni al thread principale.

Utilizzare i gestori

Puoi utilizzare un Gestore per accodare un'azione da eseguire su un altro . Per specificare il thread su cui eseguire l'azione, crea la Handler utilizzando un Looper per il thread. Un Looper è un oggetto che viene eseguito il loop di messaggi per un thread associato. Dopo aver creato un Handler, puoi quindi utilizzare il metodo post(Runnable) per eseguire un blocco di codice thread corrispondente.

Looper include una funzione helper, getMainLooper(), che recupera la Looper del thread principale. Puoi eseguire il codice nel thread principale utilizzando questo Looper per creare un Handler. Poiché si tratta di un'operazione che potresti fare abbastanza spesso, puoi anche salvare un'istanza di Handler nella stessa posizione in cui hai salvato ExecutorService:

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

È buona norma inserire il gestore nel repository, in quanto per avere una maggiore flessibilità. Ad esempio, in futuro potresti voler trasferire una Handler diverso per pianificare le attività in un thread separato. Se sei sempre comunicando allo stesso thread, puoi passare Handler al di archiviazione del repository, come mostrato nell'esempio seguente.

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

In alternativa, se vuoi maggiore flessibilità, puoi trasmettere un Handler a ogni :

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 questo esempio, il callback passato all'elemento makeLoginRequest del repository viene eseguita sul thread principale. Ciò significa che puoi modificare direttamente l'interfaccia utente dal callback o usa LiveData.setValue() per comunicare con la UI.

Configura un pool di thread

Puoi creare un pool di thread utilizzando una delle funzioni helper Executor. con impostazioni predefinite, come mostrato nel precedente codice di esempio. In alternativa, per personalizzare i dettagli del pool di thread, puoi creare utilizzando direttamente ThreadPoolExecutor. Puoi configurare quanto segue dettagli:

  • Dimensione iniziale e massima del pool.
  • Mantieni in tempo reale e unità di tempo. Il valore Keep-Alive Time indica la durata massima di un il thread può rimanere inattivo prima di arrestarsi.
  • Una coda di input che contiene Runnable attività. Questa coda deve implementare Interfaccia BlockQueue. Per soddisfare i requisiti della tua app, puoi: scegliere tra le implementazioni disponibili delle code. Per saperne di più, consulta il corso Panoramica di ThreadPoolExecutor.

Ecco un esempio che specifica la dimensione del pool di thread in base al numero totale di core di processori, un tempo di mantenimento in tempo reale di un secondo e una coda di input.

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