Panoramica dei processi e dei thread

Quando si avvia 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 nello stesso thread, chiamato thread main.

Se viene avviato un componente dell'applicazione ed esiste già un processo per l'applicazione, poiché un altro componente dell'applicazione è già stato avviato, il componente viene avviato all'interno di quel processo e utilizza lo stesso thread di esecuzione. Tuttavia, puoi organizzare l'esecuzione di componenti diversi dell'applicazione in processi separati e creare thread aggiuntivi per qualsiasi processo.

Questo documento illustra il funzionamento di processi e thread in un'applicazione Android.

Processi

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

La voce del file manifest per ogni tipo di elemento del componente (<activity>, <service>, <receiver> e <provider>) supporta un attributo android:process in grado di specificare un processo in cui viene eseguito il componente. Puoi impostare questo attributo in modo che ogni componente venga eseguito nel proprio processo o che alcuni componenti condividano un processo e 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'elemento <application> supporta anche un attributo android:process, che puoi utilizzare per impostare un valore predefinito da applicare a tutti i componenti.

Android potrebbe decidere di arrestare un processo a un certo punto, quando altri processi richiedono risorse che vengono fornite più immediatamente all'utente. Di conseguenza, i componenti dell'applicazione in esecuzione nel processo arrestato vengono eliminati. Viene avviato di nuovo un processo per quei componenti quando hanno del lavoro da fare.

Per decidere quali processi arrestare, il sistema Android ne valuta l'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 interrompere un processo, pertanto, dipende dallo stato dei componenti in esecuzione in quel processo.

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

Thread

All'avvio di un'applicazione, il sistema crea un thread di esecuzione per l'applicazione, chiamato thread principale. Questo thread è molto importante perché è incaricato dell'invio di eventi ai widget dell'interfaccia utente appropriati, inclusi gli eventi di disegno. È anche quasi sempre il thread in cui la tua applicazione interagisce con i componenti dei pacchetti android.widget e android.view del toolkit dell'interfaccia utente di Android. Per questo motivo, a volte il thread principale è chiamato thread UI. Tuttavia, in circostanze speciali, il thread principale di un'app potrebbe non essere il thread dell'interfaccia utente. Per maggiori informazioni, consulta la pagina relativa alle annotazioni di Thread.

Il sistema non crea un thread separato per ogni istanza di un componente. Tutti i componenti eseguiti nello stesso processo vengono creati in un'istanza nel thread dell'interfaccia utente e da quel thread vengono inviate le chiamate di sistema a ciascun componente. Di conseguenza, i metodi che rispondono ai callback di sistema, ad esempio onKeyDown() per segnalare le azioni degli utenti 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 dell'interfaccia utente dell'app invia l'evento touch al widget, che a sua volta ne imposta lo stato premuto e pubblica una richiesta di annullamento della convalida alla coda degli eventi. Il thread dell'interfaccia utente rimuove la coda della richiesta e avvisa il widget di riprogettarlo.

Se non implementi correttamente l'applicazione, questo modello a thread unico può produrre prestazioni scadenti quando la tua 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 sul database, blocca l'intera UI. Quando il thread è bloccato, non può essere inviato alcun evento, inclusi gli eventi di disegno.

Dal punto di vista dell'utente, l'applicazione sembra bloccarsi. Ancora peggio, se il thread dell'interfaccia utente viene bloccato per più di alcuni secondi, all'utente viene visualizzata la finestra di dialogo "L'applicazione non risponde" (ANR). L'utente potrebbe quindi decidere di uscire dall'applicazione o persino disinstallarla.

Tieni presente che il toolkit per la UI di Android non è compatibile con i thread. Pertanto, non manipolare l'interfaccia utente da un thread di lavoro. Modifica l'interfaccia utente dal thread della UI. Il modello single-thread di Android prevede due regole:

  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 worker

A causa di questo modello a thread singoli, è fondamentale per la reattività dell'interfaccia utente della tua applicazione che non blocchi il thread dell'interfaccia utente. Se devi eseguire operazioni che non sono istantanee, assicurati di eseguirle in thread in background o worker separati. Ricorda solo che puoi aggiornare l'interfaccia utente da un thread diverso dall'interfaccia utente o dal thread principale.

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

L'esempio seguente utilizza View.post(Runnable):

Kotlin

fun onClick(v: View) {
    Thread(Runnable {
        // A potentially time consuming task.
        val bitmap = processBitMap("image.png")
        imageView.post {
            imageView.setImageBitmap(bitmap)
        }
    }).start()
}

Java

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            // A potentially time consuming task.
            final Bitmap bitmap =
                    processBitMap("image.png");
            imageView.post(new Runnable() {
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

Questa implementazione è a misura di thread, 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 considerare l'utilizzo di Handler nel thread di lavoro per elaborare i messaggi recapitati 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 Panoramica del lavoro in background.

Metodi compatibili con i thread

In alcune situazioni, i metodi che implementi vengono richiamati da più thread, pertanto devono essere scritti in modo da garantire la sicurezza dei thread.

Questo vale principalmente per i metodi che possono essere chiamati da remoto, come i metodi in 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 mantiene nello stesso processo di IBinder. Non viene eseguita nel thread dell'interfaccia utente del processo.

Ad esempio, mentre il metodo onBind() di un servizio viene chiamato dal thread UI del processo del servizio, i metodi implementati nell'oggetto restituito da onBind(), come una sottoclasse che implementa metodi di chiamata di procedura remota (RPC), vengono richiamati dai thread nel pool. Poiché un servizio può avere più client, più thread del pool possono utilizzare contemporaneamente lo stesso metodo IBinder, pertanto i metodi IBinder devono essere implementati per essere sicuri per i thread.

Analogamente, un fornitore di contenuti può ricevere richieste di dati che hanno origine in altri processi. Le classi ContentResolver e ContentProvider nascondono i dettagli di come viene gestita la comunicazione tra processi (IPC), ma i metodi ContentProvider che rispondono a queste richieste (i metodi query(), insert(), delete(), update() e getType()) vengono richiamati da un pool di thread nel processo del fornitore di contenuti, non dal thread della UI per il processo. Poiché questi metodi potrebbero essere chiamati contemporaneamente da un numero qualsiasi di thread, anch'essi devono essere implementati per essere thread-safe.

Comunicazione tra processi

Android offre un meccanismo per IPC che utilizza le RPC, in cui un metodo viene chiamato da un'attività o da un altro componente dell'applicazione ma eseguito in remoto in un altro processo, con qualsiasi risultato restituito al chiamante. Ciò comporta la scomposizione di una chiamata al metodo e dei relativi dati a un livello che il sistema operativo è in grado di comprendere, trasmettendoli dallo spazio degli indirizzi e dei processi locali allo spazio degli indirizzi e del processo remoto, per poi riassemblare e riproporre la chiamata lì.

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

Per eseguire l'IPC, l'applicazione deve essere associata a un servizio utilizzando bindService(). Per ulteriori informazioni, consulta la panoramica dei servizi.