Migliori prestazioni tramite l'organizzazione in thread

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.