Su Android, un uso esperto dei thread può aiutarti a migliorare le prestazioni dei dispositivi. In questa pagina vengono trattati diversi aspetti dell'utilizzo dei thread: lavorare con l'interfaccia utente (principale), la relazione tra il ciclo di vita dell'app la priorità dei thread; e i metodi offerti dalla piattaforma per aiutare a gestire complessità. In ciascuna di queste aree, in questa pagina vengono descritte le potenziali insidie e strategie per evitarli.
Thread principale
Quando l'utente avvia l'app, Android crea una nuova finestra Linux processo insieme a un thread di esecuzione. Questo thread principale chiamato anche thread UI, è responsabile di tutto ciò che accade sullo schermo. Capire come funziona può aiutarti a progettare la tua app affinché utilizzi thread principale per ottenere le migliori prestazioni possibili.
Interni
Il thread principale ha un design molto semplice: il suo unico compito è prendere ed eseguire blocchi di lavoro da una coda di lavoro sicura per thread fino al termine dell'app. La di lavoro genera alcuni di questi blocchi di lavoro in vari contesti. Questi includono callback associati a informazioni sul ciclo di vita, eventi utente come come input o eventi provenienti da altre app e processi. Inoltre, l'app può accodare esplicitamente i blocchi in modo autonomo, senza utilizzare il framework.
Quasi qualsiasi blocco di codice eseguito dalla tua app è legato a un callback di eventi, come input, l'inflazione del layout o disegnare. Quando qualcosa attiva un evento, il thread in cui l'evento successo invia l'evento al di fuori di se stesso e al messaggio del thread principale in coda. Il thread principale può quindi gestire l'evento.
Durante un'animazione o un aggiornamento dello schermo, il sistema tenta di eseguire un blocco di lavoro (che è responsabile di disegnare lo schermo) ogni 16 ms circa, in per un rendering più fluido a 60 frame al secondo. Affinché il sistema raggiunga questo obiettivo, la gerarchia UI/Visualizzazioni devono essere aggiornati sul thread principale. Tuttavia, quando la coda di messaggi del thread principale contiene attività troppo numerose o troppo lunghe perché il thread principale completare l'aggiornamento in fretta, l'app dovrebbe trasferire questo lavoro a un worker . Se il thread principale non può terminare l'esecuzione di blocchi di lavoro entro 16 ms, l'utente potrebbe osservare intoppi, ritardi o una mancanza di reattività dell'interfaccia utente all'input. Se il thread principale si blocca per circa cinque secondi, il sistema visualizza l'applicazione Finestra di dialogo Non risponde (ANR), che consente all'utente di chiudere direttamente l'app.
Spostare attività numerose o lunghe dal thread principale, in modo che non interferiscano con un rendering fluido e una veloce reattività all'input dell'utente, è la soluzione perché tu abbia deciso di adottare l'organizzazione in thread nella tua app.
Thread e riferimenti agli oggetti UI
Per definizione, Android Gli oggetti vista non sono sicuri per i thread. Un'app deve creare, utilizzare e o eliminare gli oggetti UI, tutto sul thread principale. Se provi a modificare o fare riferimento a un oggetto UI in un thread diverso dal thread principale, Possono essere eccezioni, errori silenziosi, arresti anomali e altri comportamenti anomali non definiti.
I problemi relativi ai riferimenti rientrano in due categorie distinte: riferimenti espliciti e riferimenti impliciti.
Riferimenti espliciti
Molte attività nei thread non principali hanno l'obiettivo finale di aggiornare gli oggetti UI. Tuttavia, se uno di questi thread accede a un oggetto nella gerarchia delle viste, dell'instabilità dell'applicazione può provocare: se un thread worker modifica le proprietà nello stesso momento in cui qualsiasi altro thread fa riferimento all'oggetto, i risultati non sono definiti.
Ad esempio, considera un'app che contiene un riferimento diretto a un oggetto UI su un
e il thread di lavoro. L'oggetto nel thread di lavoro può contenere un riferimento a un
View
; ma prima del completamento del lavoro, View
rimosso dalla gerarchia di visualizzazione. Quando queste due azioni si verificano contemporaneamente,
il riferimento mantiene l'oggetto View
in memoria e imposta le proprietà al suo interno.
Tuttavia, l'utente non vede mai
e l'app lo elimina una volta eliminato il riferimento.
In un altro esempio, gli oggetti View
contengono riferimenti all'attività
proprietario. Se
viene eliminata l'attività, ma rimane un blocco di lavori in thread che
vi fa riferimento, direttamente o indirettamente, il garbage collector non raccoglierà
l'attività fino al termine dell'esecuzione del blocco di lavoro.
Questo scenario può causare un problema nelle situazioni in cui il lavoro in thread potrebbe trovarsi
quando si verifica un evento del ciclo di vita di un'attività, come la rotazione dello schermo.
Il sistema non sarà in grado di eseguire la garbage collection finché l'istanza non era in corso
il lavoro viene completato. Di conseguenza, potrebbero essere presenti due oggetti Activity
fino a quando non viene eseguita la garbage collection.
Con scenari come questi, suggeriamo di non includere contenuti espliciti riferimenti agli oggetti UI nelle attività di lavoro in thread. Evitando questi riferimenti puoi evitare questi tipi di perdite di memoria, evitando al tempo stesso i conflitti di thread.
In tutti i casi, l'app deve aggiornare gli oggetti UI solo nel thread principale. Questo usa le norme di negoziazione che permettano a più thread di comunicare il lavoro al thread principale, ovvero l'attività principale con il lavoro di aggiornamento dell'effettivo oggetto UI.
Riferimenti impliciti
Un errore comune nella progettazione del codice con gli oggetti in thread può essere visto nello snippet di seguente:
Kotlin
class MainActivity : Activity() { // ... inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() { override fun doInBackground(vararg params: Unit): String {...} override fun onPostExecute(result: String) {...} } }
Java
public class MainActivity extends Activity { // ... public class MyAsyncTask extends AsyncTask<Void, Void, String> { @Override protected String doInBackground(Void... params) {...} @Override protected void onPostExecute(String result) {...} } }
Il difetto di questo snippet è che il codice dichiara l'oggetto di thread
MyAsyncTask
come classe interna non statica di alcune attività (o una classe interna
in Kotlin). Questa dichiarazione crea un riferimento implicito all'oggetto Activity
che lo contiene
in esecuzione in un'istanza Compute Engine. Di conseguenza, l'oggetto contiene un riferimento all'attività fino a quando
il lavoro in thread viene completato, causando un ritardo nell'eliminazione dell'attività di riferimento.
Questo ritardo, a sua volta, esercita maggiore pressione sulla memoria.
Una soluzione diretta a questo problema sarebbe definire la classe di sovraccarico di Compute Engine come classi statiche o nei propri file, rimuovendo così il riferimento implicito.
Un'altra soluzione potrebbe essere quella di annullare sempre e ripulire le attività in background nei
Callback del ciclo di vita Activity
, ad esempio onDestroy
. Questo approccio può essere
noiosa e soggetta a errori. Come regola generale, non utilizzare una logica complessa che non corrisponde all'interfaccia utente
direttamente nelle attività. Inoltre, AsyncTask
è ora deprecato ed è
l'utilizzo nel nuovo codice non è consigliato. Consulta la sezione Threading su Android
per maggiori dettagli sulle primitive di contemporaneità a tua disposizione.
Thread e cicli di vita delle attività nelle app
Il ciclo di vita dell'app può influire sul funzionamento dei thread nella tua applicazione. Potresti dover decidere se un thread deve o meno persistere dopo viene eliminata l'attività. Devi inoltre conoscere il rapporto tra la prioritizzazione dei thread e se un'attività è in esecuzione in primo piano sfondo.
Thread persistenti
I fili persistono anche oltre la durata delle attività che li hanno generati. Fili continueranno a essere eseguiti, senza interruzioni, indipendentemente dalla creazione o distruzione dei attività, anche se verranno terminate con la procedura di richiesta una volta che non ci saranno componenti dell'applicazione più attivi. In alcuni casi, questa persistenza è auspicabile.
Considera un caso in cui un'attività genera un insieme di blocchi di lavoro con thread e viene poi eliminato prima che un thread worker possa eseguire i blocchi. Quale deve essere la un'app che fa con i blocchi in volo?
Se i blocchi aggiornano una UI che non esiste più, non c'è motivo affinché il lavoro continui. Ad esempio, se devi caricare informazioni sugli utenti da un database e quindi aggiornare le visualizzazioni, il thread non è più necessario.
Al contrario, i pacchetti di lavoro possono avere qualche vantaggio non interamente correlato
nell'interfaccia utente. In questo caso, devi mantenere il thread. Ad esempio, i pacchetti possono essere
in attesa di scaricare un'immagine, memorizzarla nella cache su disco e aggiornare
View
oggetto. Sebbene l'oggetto non esista più, le operazioni di download e
memorizzare nella cache l'immagine può essere comunque utile, nel caso in cui l'utente ritorni
ha eliminato l'attività.
La gestione manuale delle risposte del ciclo di vita per tutti gli oggetti in thread può diventare
estremamente complessi. Se non li gestisci correttamente, la tua app può soffrire
contese e problemi di prestazioni della memoria. Combinazione
ViewModel
con LiveData
ti consente di
caricare i dati e ricevere una notifica quando cambia
senza doversi preoccupare del ciclo di vita.
Gli oggetti ViewModel
sono
una soluzione a questo problema. I ViewModel vengono gestiti durante tutte le modifiche alla configurazione,
fornisce un modo semplice per rendere persistenti i dati della vista. Per ulteriori informazioni su ViewModels, consulta
Guida di ViewModel e scoprire di più su
Consulta la guida di LiveData. Se
per ulteriori informazioni sull'architettura dell'applicazione, leggi le
Guida all'architettura delle app.
Priorità thread
Come descritto in Processi e il ciclo di vita dell'applicazione, la priorità ricevuta dai thread dell'app dipende in parte dalla fase del suo ciclo di vita in cui si trova l'app. Quando crei gestire i thread nella tua applicazione, è importante impostarne la priorità in modo che i thread giusti ottengono le giuste priorità al momento giusto. Se il valore impostato è troppo alto, il tuo thread potrebbe interrompere il thread dell'interfaccia utente e RenderThread, facendo sì che l'app l'eliminazione dei frame. Se il valore impostato è troppo basso, puoi eseguire le attività asincrone (ad esempio, vengono caricati più lentamente del necessario.
Ogni volta che crei un thread, devi chiamare
setThreadPriority()
.
Il thread del sistema
scheduler dà la preferenza ai thread con priorità elevate, bilanciandoli
priorità, con la necessità di portare a termine tutto il lavoro. In genere, i thread
in primo piano
di ottenere circa il 95% del tempo di esecuzione totale dal dispositivo, mentre
in background raggiunge circa il 5%.
Inoltre, il sistema assegna a ogni thread un proprio valore di priorità, utilizzando il metodo
Process
corso.
Per impostazione predefinita, il sistema imposta la priorità di un thread sulla stessa priorità e sullo stesso gruppo
appartenenze come thread di generazione. Tuttavia, la tua applicazione può indicare
regola la priorità dei thread utilizzando
setThreadPriority()
.
Process
aiuta a ridurre la complessità nell'assegnazione di valori di priorità fornendo una
insieme di costanti che la tua app può utilizzare per impostare le priorità dei thread. Ad esempio:
THREAD_PRIORITY_DEFAULT
rappresenta il valore predefinito per un thread. La tua app deve impostare la priorità del thread su
THREAD_PRIORITY_BACKGROUND
per i thread che eseguono lavori meno urgenti.
La tua app può usare THREAD_PRIORITY_LESS_FAVORABLE
e THREAD_PRIORITY_MORE_FAVORABLE
come fattori di incremento per impostare le priorità relative. Per un elenco di
le priorità dei thread, consulta
THREAD_PRIORITY
costante in
la classe Process
.
Per ulteriori informazioni
gestire i thread, consulta la documentazione di riferimento
Thread
e Process
corsi.
Classi di supporto per l'organizzazione in thread
Per gli sviluppatori che utilizzano Kotlin come lingua principale, consigliamo di utilizzare le coroutine. Le coroutine offrono numerosi vantaggi, tra cui scrivere codice asincrono senza callback e contemporaneità strutturata per definizione dell'ambito, cancellazione e gestione degli errori.
Il framework fornisce inoltre le stesse classi e primitive Java per facilitare
thread, ad esempio Thread
, Runnable
e Executors
corsi,
nonché altri, come HandlerThread
.
Per saperne di più, consulta la pagina relativa alla Threading su Android.
La classe GestoriThread
Un thread gestore è in pratica un thread a lunga esecuzione che recupera il lavoro da una coda e opera .
Considera un problema comune relativo al recupero di fotogrammi di anteprima da
Camera
oggetto.
Quando ti registri per le cornici di anteprima della fotocamera, le ricevi nella
onPreviewFrame()
che viene richiamato nel thread dell'evento da cui è stata chiamata. Se questo
richiamati nel thread dell'interfaccia utente, l'attività di gestione
interferiscono con il lavoro di rendering e di elaborazione degli eventi.
In questo esempio, quando la tua app delega il comando Camera.open()
a una
blocco di lavoro sul thread gestore, l'elemento associato
onPreviewFrame()
callback
arriva al thread gestore, anziché al thread dell'interfaccia utente. Quindi, se hai intenzione di creare campagne
lavorare sui pixel, questa potrebbe essere la soluzione migliore.
Quando la tua app crea un thread utilizzando HandlerThread
, non
dimentica di impostare il livello
la priorità in base al tipo di lavoro che sta svolgendo. Ricorda che le CPU possono solo
gestire un numero ridotto di thread in parallelo. L'impostazione della priorità è utile
il sistema conosca i modi giusti per pianificare questo lavoro quando tutti gli altri thread
combattono per attirare l'attenzione.
La classe ThreadPoolExecutor
Esistono alcuni tipi di lavori che possono essere ridotti a molti paralleli,
più attività distribuite. Ad esempio, il calcolo di un filtro per ogni
Blocco 8 x 8 di un'immagine da 8 megapixel. Data l'enorme quantità di pacchetti di lavoro,
creati, HandlerThread
non è il corso appropriato da usare.
ThreadPoolExecutor
è un corso per aiutare a rendere
questo processo. Questa classe gestisce la creazione di un gruppo di thread,
le loro priorità e gestisce il modo in cui il lavoro viene distribuito tra i thread.
Man mano che il carico di lavoro aumenta o diminuisce, la classe avvia o distrugge altri thread.
per adattarsi al carico di lavoro.
Inoltre, questo corso aiuta la tua app a generare un numero ottimale di thread. Quando
genera un ThreadPoolExecutor
, l'app imposta un valore minimo e massimo
di thread. Poiché il carico di lavoro assegnato
ThreadPoolExecutor
aumenta,
la classe prenderà il numero minimo e massimo di thread inizializzati in
l'account di servizio e considerare la quantità di lavoro
in sospeso da fare. In base a questi
fattori, ThreadPoolExecutor
decide quanti
i thread devono essere attivi in qualsiasi momento.
Quanti thread dovresti creare?
Anche se a livello software, il tuo codice è in grado di creare centinaia di questo può creare problemi di prestazioni. L'app condivide una CPU limitata risorse con servizi in background, il renderer, il motore audio networking e altro ancora. Le CPU hanno in realtà solo possibilità di gestire un numero ridotto di thread in parallelo; tutto ciò che viene eseguito in un problema di priorità e pianificazione. Per questo è importante creare solo il numero di thread necessari per il carico di lavoro.
In pratica, ci sono varie variabili responsabili di tutto, ma scegliere un valore (ad esempio 4, per i comandi iniziali) e testarlo Systrace è come una strategia solida come tutte le altre. Puoi usare tentativi di errore per scoprire il numero minimo di thread che puoi utilizzare senza riscontrare problemi.
Un'altra considerazione da considerare nel decidere quanti thread avere è che i thread non sono senza costi: occupano memoria. Ogni thread ha un costo minimo di 64.000 memoria. Questo si somma rapidamente alle numerose app installate su un dispositivo, soprattutto in cui gli stack di chiamate crescono in modo significativo.
Molti processi di sistema e librerie di terze parti avviano spesso e i pool di thread. Se la tua app può riutilizzare un pool di thread esistente, questo riutilizzo potrebbe aiutarti delle prestazioni riducendo i conflitti per le risorse di memoria e di elaborazione.