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