Panoramica dei processi e dei thread

Quando viene avviato un componente dell'applicazione e l'applicazione non ha altri componenti in esecuzione, il sistema Android avvia un nuovo processo Linux per l'applicazione con un singolo thread di esecuzione. Per impostazione predefinita, tutti i componenti della stessa applicazione vengono eseguiti nello stesso processo e thread, chiamato thread principale.

Se viene avviato un componente dell'applicazione ed è già presente un processo per l'applicazione, perché è già stato avviato un altro componente dell'applicazione, il componente viene avviato all'interno di quel processo e utilizza lo stesso thread di esecuzione. Tuttavia, puoi fare in modo che i diversi componenti dell'applicazione vengano eseguiti in processi separati e puoi creare thread aggiuntivi per qualsiasi processo.

Questo documento descrive il funzionamento di processi e thread in un'app per Android.

Processi

Per impostazione predefinita, tutti i componenti di un'applicazione vengono eseguiti nello stesso processo e la maggior parte delle applicazioni non modifica questa impostazione. Tuttavia, se ritieni di dover controllare il processo a cui appartiene un determinato componente, puoi farlo nel file manifest.

La voce del manifest per ogni tipo di elemento componente, <activity>, <service>, <receiver> e <provider>, supporta un attributo android:process che può specificare un processo in cui viene eseguito il componente. Puoi impostare questo attributo in modo che ogni componente venga eseguito nel proprio processo o in modo che alcuni componenti condividano un processo, mentre altri no.

Puoi anche impostare android:process in modo che i componenti di applicazioni diverse vengano eseguiti nello stesso processo, a condizione che le applicazioni condividano lo stesso ID utente Linux e siano firmate con gli stessi certificati.

L'<application> elemento supporta anche un attributo android:process, che puoi utilizzare per impostare un valore predefinito che si applica a tutti i componenti.

Android potrebbe decidere di arrestare un processo a un certo punto, quando le risorse sono richieste da altri processi che servono l'utente in modo più immediato. Di conseguenza, i componenti dell'applicazione in esecuzione nel processo arrestato vengono eliminati. Viene avviato di nuovo un processo per questi componenti quando devono svolgere un'attività.

Quando decide quali processi arrestare, il sistema Android valuta la loro importanza relativa per l'utente. Ad esempio, arresta più facilmente un processo che ospita attività non più visibili sullo schermo rispetto a un processo che ospita attività visibili. La decisione di terminare un processo dipende quindi dallo stato dei componenti in esecuzione in quel processo.

I dettagli del ciclo di vita del processo e della sua relazione con gli stati dell'applicazione sono descritti in Processi e ciclo di vita dell'app.

Thread

Quando viene avviata un'applicazione, il sistema crea un thread di esecuzione per l'applicazione, chiamato thread principale. Questo thread è molto importante, perché è responsabile dell'invio degli eventi ai widget dell'interfaccia utente appropriati, inclusi gli eventi di disegno. È anche quasi sempre il thread in cui l'applicazione interagisce con i componenti dei pacchetti android.widget e android.view del toolkit per la UI di Android. Per questo motivo, il thread principale viene a volte chiamato thread dell'interfaccia utente. Tuttavia, in circostanze speciali, il thread principale di un'app potrebbe non essere il thread dell'interfaccia utente. Per saperne di più, consulta Annotazioni dei thread.

Il sistema non crea un thread separato per ogni istanza di un componente. Tutti i componenti che vengono eseguiti nello stesso processo vengono creati un'istanza nel thread dell'interfaccia utente e le chiamate di sistema a ogni componente vengono inviate da quel thread. Di conseguenza, i metodi che rispondono ai callback di sistema, come onKeyDown() per segnalare le azioni dell'utente o un metodo di callback del ciclo di vita, vengono sempre eseguiti nel thread dell'interfaccia utente del processo.

Ad esempio, quando l'utente tocca un pulsante sullo schermo, il thread della UI dell'app invia l'evento touch al widget, che a sua volta imposta il suo stato di pressione e pubblica una richiesta di invalidazione nella coda degli eventi. Il thread dell'interfaccia utente rimuove la richiesta dalla coda e notifica al widget di ridisegnarsi.

A meno che tu non implementi correttamente l'applicazione, questo modello a thread singolo può comportare prestazioni scadenti quando l'app esegue un lavoro intensivo in risposta all'interazione dell'utente. L'esecuzione di operazioni lunghe nel thread dell'interfaccia utente, come l'accesso alla rete o le query di database, blocca l'intera UI. Quando il thread è bloccato, non è possibile inviare eventi, inclusi gli eventi di disegno.

Dal punto di vista dell'utente, l'applicazione sembra smettere di rispondere. Ancora peggio, se il thread dell'interfaccia utente è bloccato per più di qualche secondo, all'utente viene mostrata la finestra di dialogo "L'applicazione non risponde" (ANR). L'utente potrebbe quindi decidere di chiudere l'applicazione o persino disinstallarla.

Tieni presente che il toolkit per la UI di Android non è thread-safe. Pertanto, non manipolare la UI da un thread di lavoro. Esegui tutte le manipolazioni dell'interfaccia utente dal thread della UI. Esistono due regole per il modello a thread singolo di Android:

  1. Non bloccare il thread dell'interfaccia utente.
  2. Non accedere al toolkit per la UI di Android dall'esterno del thread dell'interfaccia utente.

Thread di lavoro

A causa di questo modello a thread singolo, è fondamentale per la reattività della UI dell'applicazione non bloccare il thread dell'interfaccia utente. Se devi eseguire operazioni non istantanee, assicurati di eseguirle in thread in background o di lavoro separati. Ricorda che non puoi aggiornare la UI da nessun thread diverso dal thread della UI o principale.

Per aiutarti a rispettare queste regole, Android offre diversi modi per accedere al thread dell'interfaccia utente da altri thread. Ecco un elenco di metodi che possono aiutarti:

Gli esempi seguenti mostrano come trasferire un'attività a un thread in background e aggiornare il thread dell'interfaccia utente al termine dell'attività:

Kotlin

// Kotlin coroutines implementation.
fun onClick(v: View) {
    // Launch a coroutine in the lifecycle scope (e.g., in an Activity or Fragment).
    lifecycleScope.launch {
        // Run the blocking task on the IO dispatcher.
        val bitmap = withContext(Dispatchers.IO) {
            BitmapFactory.decodeFile("image.png")
        }
        // Back on the main thread, update the UI.
        imageView.setImageBitmap(bitmap)
    }
}

Java

// Java Executor implementation.
// (executorService is assumed to be defined elsewhere).
public void onClick(View v) {
    executorService.execute(() -> {
        // Run the heavy task on a background thread.
        Bitmap bitmap = BitmapFactory.decodeFile("image.png");

        // Update the View on the UI thread.
        imageView.post(() -> imageView.setImageBitmap(bitmap));
    });
}

Questa implementazione è thread-safe, perché l'operazione in background viene eseguita da un thread separato, mentre ImageView viene sempre manipolato dal thread dell'interfaccia utente.

Tuttavia, man mano che la complessità dell'operazione aumenta, questo tipo di codice può diventare complicato e difficile da gestire. Per gestire interazioni più complesse con un thread di lavoro, potresti prendere in considerazione l'utilizzo di un Handler nel thread di lavoro per elaborare i messaggi inviati dal thread dell'interfaccia utente. Per una spiegazione completa di come pianificare il lavoro sui thread in background e comunicare con il thread dell'interfaccia utente, consulta la panoramica del lavoro in background.

Metodi thread-safe

In alcune situazioni, i metodi che implementi vengono chiamati da più di un thread e, pertanto, devono essere scritti in modo da essere thread-safe.

Questo vale soprattutto per i metodi che possono essere chiamati da remoto, ad esempio i metodi di un servizio associato. Quando una chiamata a un metodo implementato in un IBinder ha origine nello stesso processo in cui è in esecuzione IBinder, il metodo viene eseguito nel thread del chiamante. Tuttavia, quando la chiamata ha origine in un altro processo, il metodo viene eseguito in un thread scelto da un pool di thread che il sistema gestisce nello stesso processo di IBinder. Non viene eseguito nel thread dell'interfaccia utente del processo.

Ad esempio, mentre il metodo onBind() di un servizio viene chiamato dal thread dell'interfaccia utente del processo del servizio, i metodi implementati nell'oggetto restituito da onBind() come una sottoclasse che implementa i metodi di chiamata di procedura remota (RPC), vengono chiamati dai thread del pool. Poiché un servizio può avere più di un client, più di un thread del pool può coinvolgere lo stesso metodo IBinder contemporaneamente, quindi i metodi IBinder devono essere implementati in modo da essere thread-safe.

Allo stesso modo, un content provider può ricevere richieste di dati provenienti da altri processi. Le classi ContentResolver e ContentProvider nascondono i dettagli della gestione della comunicazione interprocesso (IPC), ma i metodi ContentProvider che rispondono a queste richieste, ovvero i metodi query(), insert(), delete(), update(), e getType(), vengono chiamati da un pool di thread nel processo del content provider, non dal thread della UI del processo. Poiché questi metodi potrebbero essere chiamati da un numero qualsiasi di thread contemporaneamente, anche questi devono essere implementati in modo da essere thread-safe.

Comunicazione tra processi (Inter-Process Communication, IPC)

Android offre un meccanismo per l'IPC che utilizza le RPC, in cui un metodo viene chiamato da un'attività o da un altro componente dell'applicazione, ma viene eseguito da remoto in un altro processo, con qualsiasi risultato restituito al chiamante. Ciò comporta la scomposizione di una chiamata di metodo e dei relativi dati a un livello comprensibile per il sistema operativo, la trasmissione dal processo locale e dallo spazio degli indirizzi al processo remoto e allo spazio degli indirizzi, quindi il riassemblaggio e la riproduzione della chiamata.

I valori restituiti vengono quindi trasmessi nella direzione opposta. Android fornisce tutto il codice per eseguire queste transazioni IPC, in modo che tu possa concentrarti sulla definizione e sull'implementazione dell'interfaccia di programmazione RPC.

Per eseguire l'IPC, l'applicazione deve eseguire l'associazione a un servizio utilizzando bindService(). Per saperne di più, consulta la panoramica dei servizi.