Processi e thread

Quickview

  • Ogni applicazione viene eseguita in un proprio processo e tutti i componenti dell’applicazione vengono eseguiti in quel processo, per default
  • Ogni operazione lenta e bloccante in un’attività dovrebbe essere eseguita in un nuovo thread, per evitare di rallentare l’interfaccia utente

In questo documento

  1. Processi
    1. Ciclo di vita del processo
  2. Threads
    1. Worker threads
    2. Thread-safe methods
  3. Interprocess Communication

Quando un componente dell’applicazione si avvia 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 un componente dell’applicazione si avvia ed esiste già un processo per quell’applicazione (perché esiste un altro componente dell’applicazione), allora il componente viene avviato all’interno di quel processo e utilizza lo stesso thread di esecuzione. Tuttavia, potete fare in modo che diversi componenti della vostra applicazione vengano eseguiti in processi separati, e potete creare thread aggiuntivi per qualsiasi processo.

Questo documento discute come funzionano i processi e i thread in un’applicazione Android.

Processi

Di default, tutti i componenti della stessa applicazione vengono eseguiti nello stesso processo e la maggior parte delle applicazioni non dovrebbe cambiarlo. Tuttavia, se trovate che avete bisogno di controllare a quale processo appartiene un certo componente, potete 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 quel componente dovrebbe essere eseguito. Potete 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. Si può anche impostareandroid:process in modo che i componenti di diverse applicazioni 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, per impostare un valore predefinito che si applica a tutti i componenti.

Android potrebbe decidere di chiudere un processo ad un certo punto, quando la memoria è bassa e richiesta da altri processi che servono più immediatamente l’utente. I componenti dell’applicazione in esecuzione nel processo che viene ucciso vengono di conseguenza distrutti. Un processo viene avviato di nuovo per quei componenti quando c’è di nuovo del lavoro da fare per loro.

Quando si decide quali processi uccidere, il sistema Android pesa la loro importanza relativa per l’utente. Per esempio, spegne più facilmente un processo che ospita attività che non sono più visibili sullo schermo, rispetto a un processo che ospita attività visibili. La decisione di terminare o meno un processo, quindi, dipende dallo stato dei componenti in esecuzione in quel processo. Le regole usate per decidere quali processi terminare sono discusse di seguito.

Ciclo di vita del processo

Il sistema Android cerca di mantenere un processo applicativo il più a lungo possibile, ma alla fine deve rimuovere i vecchi processi per recuperare la memoria per processi nuovi o più importanti. Per determinare quali processi mantenere e quali uccidere, il sistema inserisce ogni processo in una “gerarchia di importanza” basata sui componenti in esecuzione nel processo e lo stato di questi componenti. I processi con l’importanza più bassa vengono eliminati per primi, poi quelli con l’importanza successiva più bassa, e così via, come necessario per recuperare le risorse del sistema.

Ci sono cinque livelli nella gerarchia di importanza. La seguente lista presenta i diversi tipi di processi in ordine di importanza (il primo processo è il più importante e viene eliminato per ultimo):

  1. Processo in primo piano

    Un processo che è necessario per quello che l’utente sta facendo. Un processo è considerato in primo piano se una delle seguenti condizioni è vera:

    • Ospita un Activity con cui l’utente sta interagendo (il metodo onResume() del Activity è stato chiamato).
    • Ospita un Service che è legato all’attività con cui l’utente sta interagendo.
    • Ospita un Service che è in esecuzione “in primo piano” – il servizio ha chiamato startForeground().
    • Ospita un Service che sta eseguendo uno dei suoi lifecyclecallback (onCreate(), onStart(), o onDestroy()).
    • Ospita un BroadcastReceiver che sta eseguendo il suo metodo onReceive().

    Generalmente, solo pochi processi in primo piano esistono in un dato momento. Vengono uccisi solo come ultima risorsa – se la memoria è così bassa che non possono continuare a funzionare tutti. Generalmente, a quel punto, il dispositivo ha raggiunto uno stato di paginazione della memoria, quindi uccidere alcuni processi in primo piano è necessario per mantenere l’interfaccia utente reattiva.

  2. Processo visibile

    Un processo che non ha alcun componente in primo piano, ma può ancora influenzare ciò che l’utente vede sullo schermo. Un processo è considerato visibile se una delle seguenti condizioni è vera:

    • Ospita un Activity che non è in primo piano, ma è ancora visibile all’utente (il suo metodo onPause() è stato chiamato). Questo potrebbe accadere, per esempio, se l’attività in primo piano ha iniziato una finestra di dialogo, che permette all’attività precedente di essere vista dietro di essa.
    • Ospita un Service che è legato a un’attività visibile (o in primo piano).

    Un processo visibile è considerato estremamente importante e non sarà ucciso a meno che ciò non sia richiesto per mantenere in esecuzione tutti i processi in primo piano.

  3. Processo di servizio

    Un processo che sta eseguendo un servizio che è stato avviato con il metodo startService() e non rientra in nessuna delle due categorie superiori. Anche se i processi di servizio non sono direttamente legati a qualcosa che l’utente vede, generalmente stanno facendo cose che interessano all’utente (come la riproduzione di musica in background o il caricamento di dati in rete), quindi il sistema li tiene in esecuzione a meno che non ci sia abbastanza memoria per mantenerli insieme a tutti i processi in primo piano e visibili.

  4. Processo in background

    Un processo che tiene un’attività che non è attualmente visibile all’utente (il metodoonStop() dell’attività è stato chiamato). Questi processi non hanno un impatto diretto sull’esperienza dell’utente, e il sistema può ucciderli in qualsiasi momento per recuperare la memoria per un processo precedente, visibile o di servizio. Di solito ci sono molti processi in background in esecuzione, quindi sono tenuti in una lista LRU (least recently used) per assicurare che il processo con l’attività che è stata vista più recentemente dall’utente sia l’ultimo ad essere ucciso. Se un’attività implementa correttamente i suoi metodi del ciclo di vita e salva il suo stato attuale, l’uccisione del suo processo non avrà un effetto visibile sull’esperienza dell’utente, perché quando l’utente torna all’attività, l’attività ripristina tutto il suo stato visibile. Vedere il documento Activities per informazioni sul salvataggio e il ripristino dello stato.

  5. Processo vuoto

    Un processo che non contiene alcun componente attivo dell’applicazione. L’unica ragione per mantenere in vita questo tipo di processo è per scopi di caching, per migliorare il tempo di avvio la prossima volta che un componente deve essere eseguito in esso. Il sistema spesso uccide questi processi per bilanciare le risorse complessive del sistema tra le cache dei processi e le cache del kernel sottostante.

Android classifica un processo al livello più alto possibile, in base all’importanza dei componenti attualmente attivi nel processo. Per esempio, se un processo ospita un servizio e un’attività visibile, il processo è classificato come un processo visibile, non un processo di servizio.

Inoltre, il ranking di un processo potrebbe essere aumentato perché altri processi sono dipendenti da esso-un processo che sta servendo un altro processo non può mai essere classificato più in basso del processo che sta servendo. Per esempio, se un fornitore di contenuti nel processo A sta servendo un cliente nel processo B, o se un servizio nel processo A è legato a un componente nel processo B, il processo A è sempre considerato almeno importante quanto il processo B.

Perché un processo che esegue un servizio è classificato più in alto di un processo con attività in background, un’attività che inizia un’operazione di lunga durata potrebbe fare bene ad avviare un servizio per quell’operazione, piuttosto che creare semplicemente un thread worker, in particolare se l’operazione probabilmente durerà più a lungo dell’attività.Per esempio, un’attività che sta caricando un’immagine su un sito web dovrebbe avviare un servizio per eseguire l’upload in modo che l’upload possa continuare in background anche se l’utente abbandona l’attività; usare un servizio garantisce che l’operazione abbia almeno la priorità “processo di servizio”, indipendentemente da ciò che accade all’attività. Questa è la stessa ragione per cui i ricevitori broadcast dovrebbero utilizzare servizi piuttosto che semplicemente mettere operazioni che richiedono tempo in un thread.

Threads

Quando un’applicazione viene lanciata, il sistema crea un thread di esecuzione per l’applicazione, chiamato “main”. Questo thread è molto importante perché è incaricato di distribuire gli eventi ai widget appropriati dell’interfaccia utente, compresi gli eventi di disegno. È anche il thread in cui la vostra applicazione interagisce con i componenti del toolkit UI di Android (componenti dei pacchetti android.widget e android.view). Come tale, il thread principale è anche chiamato a volte il thread UI.

Il sistema non crea un thread separato per ogni istanza di un componente. Tutti i componenti che girano nello stesso processo sono istanziati nel thread UI, e le chiamate di sistema per ogni componente sono distribuite 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 UI del processo.

Per esempio, quando l’utente tocca un pulsante sullo schermo, il thread UI della vostra app invia l’evento touch al widget, che a sua volta imposta il suo stato premuto e invia una richiesta di invalidazione alla coda di eventi. Il thread UI rimette in coda la richiesta e notifica al widget che dovrebbe ridisegnarsi.

Quando la vostra applicazione esegue un lavoro intensivo in risposta all’interazione dell’utente, questo modello a thread singolo può produrre scarse prestazioni a meno che non implementiate correttamente la vostra applicazione. In particolare, se tutto sta accadendo nel thread dell’interfaccia utente, l’esecuzione di lunghe operazioni come l’accesso alla rete o le query al database bloccherà l’intera interfaccia utente. Quando il thread è bloccato, nessun evento può essere distribuito, compresi gli eventi di disegno. Dal punto di vista dell’utente, l’applicazione sembra bloccarsi. Ancora peggio, se il thread dell’UI è bloccato per più di qualche secondo (circa 5 secondi attualmente), all’utente viene presentato il famigerato dialogo “application notresponding” (ANR). L’utente potrebbe quindi decidere di uscire dalla vostra applicazione e disinstallarla se non è soddisfatto.

Inoltre, il toolkit UI di Andoid non è thread-safe. Quindi, non dovete manipolare la vostra UI da un worker thread – dovete fare tutte le manipolazioni alla vostra interfaccia utente dall’UIthread. Quindi, ci sono semplicemente due regole per il modello a thread singolo di Android:

  1. Non bloccare il thread UI
  2. Non accedere al toolkit UI di Android dall’esterno del thread UI

File worker

A causa del modello a thread singolo descritto sopra, è vitale per la reattività dell’UI della tua applicazione che tu non blocchi il thread UI. Se avete operazioni da eseguire che non sono istantanee, dovreste assicurarvi di farle in thread separati (thread “background” o “worker”).

Per esempio, qui sotto c’è del codice per un click listener che scarica un’immagine da un thread separato e la visualizza in un ImageView:

public void onClick(View v) { new Thread(new Runnable() { public void run() { Bitmap b = loadImageFromNetwork("http://example.com/image.png"); mImageView.setImageBitmap(b); } }).start();}

Al principio, questo sembra funzionare bene, perché crea un nuovo thread per gestire l’operazione di rete. Tuttavia, viola la seconda regola del modello single-threaded: non accedere all’Android UI toolkit dall’esterno del thread UI – questo esempio modifica il ImageView dal thread worker invece che dal thread UI. Questo può risultare in un comportamento non definito e inaspettato, che può essere difficile e lungo da rintracciare.

Per risolvere questo problema, Android offre diversi modi per accedere al thread UI da altri thread. Ecco una lista di metodi che possono aiutare:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable,long)

Per esempio, è possibile risolvere il codice di cui sopra utilizzando il metodo View.post(Runnable):

public void onClick(View v) { new Thread(new Runnable() { public void run() { final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); mImageView.post(new Runnable() { public void run() { mImageView.setImageBitmap(bitmap); } }); } }).start();}

Ora questa implementazione è thread-safe: l’operazione di rete è fatta da un thread separato mentre il ImageView è manipolato dal thread dell’UI.

Tuttavia, man mano che la complessità dell’operazione cresce, questo tipo di codice può diventare complicato e difficile da mantenere. Per gestire interazioni più complesse con un worker thread, potreste considerare di usare un Handler nel vostro worker thread, per elaborare i messaggi consegnati dall’UIthread. Forse la soluzione migliore, però, è estendere la classe AsyncTask, che semplifica l’esecuzione dei compiti del worker thread che devono interagire con l’UI.

Usare AsyncTask

AsyncTask vi permette di eseguire un lavoro asincrono sulla vostra userinterface. Esegue le operazioni bloccanti in un thread di lavoro e poi pubblica i risultati sul thread dell’interfaccia utente, senza richiedervi di gestire thread e/o gestori da soli.

Per usarlo, dovete sottoclasse AsyncTask e implementare il metodo doInBackground() callback, che viene eseguito in un pool di thread in background. Per aggiornare la vostra UI, dovreste implementare onPostExecute(), che consegna il risultato di doInBackground() ed esegue nel thread dell’UI, in modo da poter aggiornare in sicurezza la vostra UI. Potete quindi eseguire il compito chiamando execute()dal thread dell’UI.

Per esempio, potete implementare l’esempio precedente usando AsyncTask in questo modo:

public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png");}private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> { /** The system calls this to perform work in a worker thread and * delivers it the parameters given to AsyncTask.execute() */ protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls); } /** The system calls this to perform work in the UI thread and delivers * the result from doInBackground() */ protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); }}

Ora l’UI è sicura e il codice è più semplice, perché separa il lavoro nella parte che dovrebbe essere fatta su un worker thread e la parte che dovrebbe essere fatta sul thread dell’UI.

Si dovrebbe leggere il riferimento AsyncTaskper una comprensione completa su come usare questa classe, ma ecco una rapida panoramica di come funziona:

  • Puoi specificare il tipo di parametri, i valori di progresso e il valore finale del compito, usando i generici
  • Il metodo doInBackground() viene eseguito automaticamente su un thread lavoratore
  • onPreExecute(), onPostExecute(), e onProgressUpdate() sono tutti invocati sul thread UI
  • Il valore restituito da doInBackground() viene inviato aonPostExecute()
  • Puoi chiamare publishProgress() in qualsiasi momento in doInBackground() per eseguire onProgressUpdate() sul thread UI
  • Puoi annullare il compito in qualsiasi momento, da qualsiasi thread

Attenzione: Un altro problema che potresti incontrare quando usi un workerthread è il riavvio inaspettato della tua attività a causa di un cambiamento di configurazione di runtime (come quando l’utente cambia l’orientamento dello schermo), che potrebbe distruggere il tuo worker thread. Per vedere come potete persistere la vostra attività durante uno di questi riavvii e come cancellare correttamente l’attività quando l’attività viene distrutta, vedete il codice sorgente dell’applicazione di esempio Shelves.

Metodi a prova di thread

In alcune situazioni, i metodi che implementate potrebbero essere chiamati da più di un thread, e quindi devono essere scritti per essere a prova di thread.

Questo è principalmente vero per i metodi che possono essere chiamati da remoto, come i metodi in un servizio vincolato. Quando una chiamata su un metodo implementato in un IBinder ha origine nello stesso processo in cui il IBinder è in esecuzione, 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 dal pool di thread che il sistema mantiene nello stesso processo del IBinder (non viene eseguito nel thread UI del processo). Per esempio, mentre il metodo onBind() di un servizio verrebbe chiamato dal thread UI del processo del servizio, i metodi implementati nell’oggetto che onBind() restituisce (per esempio, una sottoclasse che implementa metodi RPC) verrebbero chiamati dai thread del pool. Poiché un servizio può avere più di un cliente, più di un thread del pool può impegnare lo stesso metodo IBinder allo stesso tempo. I metodi IBinder devono quindi essere implementati per essere thread-safe.

Allo stesso modo, un fornitore di contenuti può ricevere richieste di dati che hanno origine in altri processi.Sebbene le classi ContentResolver e ContentProvider nascondano i dettagli di come viene gestita la comunicazione interprocesso, i metodi ContentProvider che rispondono a queste richieste – i metodi query(), insert(), delete(), update() e getType() – sono chiamati da un pool di thread nel processo del fornitore di contenuti, non dall’UIthread del processo. Poiché questi metodi potrebbero essere chiamati da un qualsiasi numero di thread nello stesso momento, anch’essi devono essere implementati per essere thread-safe.

Comunicazione interprocesso

Android offre un meccanismo per la comunicazione interprocesso (IPC) utilizzando le chiamate di procedure remote (RPC), in cui un metodo viene chiamato da un’attività o da un altro componente dell’applicazione, ma eseguito a distanza (in un altro processo), con qualsiasi risultato restituito a chi lo ha chiamato. Ciò comporta la scomposizione di una chiamata di metodo e dei suoi dati ad un livello che il sistema operativo può comprendere, trasmettendoli dal processo locale e dallo spazio degli indirizzi al processo remoto e allo spazio degli indirizzi, quindi riassemblando e riproducendo la chiamata lì. I valori di ritorno sono poi trasmessi nella direzione opposta. Android fornisce tutto il codice per eseguire queste transazioni IPC, così potete concentrarvi sulla definizione e implementazione dell’interfaccia di programmazione RPC.

Per eseguire IPC, la tua applicazione deve legarsi a un servizio, utilizzando bindService(). Per maggiori informazioni, consultate la guida per gli sviluppatori di servizi.

Per eseguire IPC, la vostra applicazione deve legarsi ad un servizio, usando bindService().

Lascia un commento