Tutte le app per Android utilizzano un thread principale per gestire le operazioni dell'interfaccia utente. La chiamata di operazioni a lunga esecuzione da questo thread principale può causare blocchi e mancata risposta. Ad esempio, se la tua app effettua una richiesta di rete dal thread principale, la UI dell'app sarà bloccata finché non riceve la risposta di rete. Se utilizzi Java, puoi creare thread in background aggiuntivi per gestire le operazioni a lunga esecuzione mentre il thread principale continua a gestire gli aggiornamenti dell'interfaccia utente.
Questa guida mostra come 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. Inoltre, mostra 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 nozioni di base dell'organizzazione in thread e dei meccanismi sottostanti. Tuttavia, esistono molte librerie note che offrono astrazioni di livello superiore rispetto a questi concetti e utilità pronte all'uso per il passaggio di dati tra i thread. Queste librerie includono Guava e RxJava per gli utenti del linguaggio di programmazione Java e Coroutines, che consigliamo per gli utenti di Kotlin.
In pratica, dovresti scegliere quella più adatta alla tua app e al tuo team di sviluppo, anche se le regole di organizzazione in thread rimangono le stesse.
Panoramica degli esempi
In base alla Guida all'architettura delle app, gli esempi in questo argomento effettuano una richiesta di rete e restituiscono il risultato al thread principale, dove l'app potrebbe quindi visualizzare il risultato sullo schermo.
In particolare, ViewModel
chiama il livello dati sul thread principale per attivare la richiesta di rete. Il livello dati ha il compito di spostare l'esecuzione della richiesta di rete dal thread principale e di pubblicare il risultato nel 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 parallelo da una coda. Vengono eseguite nuove attività su thread esistenti
quando questi thread diventano inattivi. Per inviare un'attività a un pool di thread, utilizza l'interfaccia ExecutorService
. Tieni presente che ExecutorService
non ha nulla a che fare con Services, il componente delle applicazioni Android.
La creazione di thread è costosa, quindi devi creare un pool di thread solo una volta durante l'inizializzazione dell'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 thread di quattro thread che possiamo 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 in base al carico di lavoro previsto. Per saperne di più, consulta Configurazione di un pool di thread.
Esegui in un thread in background
Se effettui una richiesta di rete nel thread principale, il thread attende, o blocca, finché non riceve una risposta. Poiché il thread è bloccato, il sistema operativo non può chiamare onDraw()
e l'app si blocca, visualizzando potenzialmente una finestra di dialogo L'applicazione non risponde (ANR). Eseguiamo invece questa operazione
su un thread in background.
Effettua la richiesta
Per prima cosa, diamo un'occhiata alla classe LoginRepository
e vediamo come esegue 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 chiamante. Per modellare la risposta alla richiesta di rete, disponiamo della nostra classe Result
.
Attiva la richiesta
ViewModel
attiva la richiesta di rete quando l'utente tocca, ad esempio, 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 effettua
la richiesta di rete. Possiamo utilizzare il pool di thread di cui abbiamo creato un'istanza per spostare
l'esecuzione in un thread in background.
Gestire l'inserimento delle dipendenze
Innanzitutto, seguendo i principi dell'inserimento delle dipendenze, LoginRepository
acquisisce un'istanza di Executor anziché ExecutorService
perché 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 utilizza un valore Runnable. 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 sposta l'esecuzione nel thread in background e ignora la risposta per il momento:
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
}
...
}
All'interno del metodo execute()
, creiamo un nuovo Runnable
con il blocco di codice che vogliamo eseguire nel thread in background, nel nostro caso il metodo di richiesta di rete sincrono. Internamente, l'oggetto ExecutorService
gestisce Runnable
ed
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 a misura di thread. Nota che nel nostro esempio evitiamo di scrivere in variabili condivise tra thread, passando invece dati immutabili. Questa è una buona pratica, perché ogni thread funziona con la propria istanza di dati ed evitiamo la complessità della sincronizzazione.
Se hai bisogno di condividere lo stato tra i thread, devi prestare attenzione a gestire l'accesso dai thread utilizzando meccanismi di sincronizzazione come i blocchi. Questo non rientra nell'ambito di questa guida. In generale, evita di condividere uno stato modificabile tra i thread, se possibile.
Comunicare con il thread principale
Nel passaggio precedente abbiamo ignorato la risposta alla richiesta di rete. Per visualizzare il risultato sullo schermo, LoginViewModel
deve essere a conoscenza dell'elemento. A tal fine, possiamo utilizzare i callback.
La funzione makeLoginRequest()
deve utilizzare un callback come parametro in modo da poter restituire un valore in modo asincrono. Il callback con il risultato viene chiamato
ogni volta che la richiesta di rete viene completata o quando si verifica un errore. In Kotlin, possiamo
utilizzare una funzione di ordine superiore. Tuttavia, in Java, dobbiamo creare una nuova interfaccia di callback per 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);
}
}
});
}
...
}
L'ViewModel
deve implementare il callback ora. Può eseguire logiche 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 della chiamata, ovvero un thread in background. Ciò significa che non puoi modificare né 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 thread. Per specificare il thread su cui eseguire l'azione, crea il
Handler
utilizzando un looer per il thread. Un Looper
è un oggetto che esegue il loop di messaggi per un thread associato. Una volta creato un Handler
, puoi utilizzare il metodo post(Runnable) per eseguire un blocco di codice nel thread corrispondente.
Looper
include una funzione helper, getMainLooper(), che recupera l'elemento
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 svolgere spesso,
puoi anche salvare un'istanza di Handler
nello stesso punto 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, dato che ti offre maggiore flessibilità. Ad esempio, in futuro potresti voler passare un Handler
diverso per pianificare attività in un thread separato. Se comunichi sempre allo stesso thread, puoi passare Handler
nel costruttore 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 inviare un Handler
a ogni funzione:
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 nella chiamata makeLoginRequest
del repository
viene eseguito nel thread principale. Ciò significa che puoi modificare l'interfaccia utente
direttamente dal callback o usare LiveData.setValue()
per comunicare con l'interfaccia.
Configura un pool di thread
Puoi creare un pool di thread utilizzando una delle funzioni helper Executor con impostazioni predefinite, come mostrato nel codice di esempio precedente. In alternativa, se vuoi personalizzare i dettagli del pool di thread, puoi creare un'istanza utilizzando direttamente ThreadPoolExecutor. Puoi configurare i seguenti dettagli:
- Dimensione iniziale e massima del pool.
- Tempo attivo e unità di tempo. Il tempo di conservazione è la durata massima per cui un thread può rimanere inattivo prima dell'arresto.
- Una coda di input che contiene
Runnable
attività. Questa coda deve implementare l'interfaccia BlockQueue. Per soddisfare i requisiti della tua app, puoi scegliere tra le implementazioni delle code disponibili. Per saperne di più, consulta la panoramica della classe ThreadPoolExecutor.
Ecco un esempio che specifica la dimensione del pool di thread in base al numero totale di core del processore, a un tempo di keep-alive di un secondo e a 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
);
...
}