Rendering lento

Il rendering dell'interfaccia utente è l'atto di generare un frame dall'app e di visualizzarlo sullo schermo. Per garantire che l'interazione di un utente con la tua app sia fluida, la tua app deve eseguire il rendering dei frame in meno di 16 ms per raggiungere i 60 frame al secondo (f/s). Per capire perché è preferibile utilizzare 60 f/s, consulta la sezione Modelli di prestazioni Android: Perché 60 f/s?. Se stai cercando di raggiungere 90 fps, la finestra scende a 11 ms, mentre per 120 f/s è di 8 ms.

Se superi questa finestra di 1 ms, non significa che il frame viene visualizzato in ritardo di 1 ms, ma Choreographer lo elimina completamente. Se la tua app presenta un rendering dell'interfaccia utente lento, il sistema è costretto a ignorare i frame e l'utente percepisce interruzioni nell'app. Questo processo è chiamato jank. Questa pagina mostra come diagnosticare e risolvere il problema di jank.

Se stai sviluppando giochi che non utilizzano il sistema View, puoi bypassare Choreographer. In questo caso, la libreria del pacing dei frame aiuta i giochi OpenGL e Vulkan a ottenere un rendering fluido e un pacing corretto dei frame su Android.

Per migliorare la qualità dell'app, Android monitora automaticamente l'app per rilevare eventuali jank e visualizza le informazioni nella dashboard Android vitals. Per informazioni su come vengono raccolti i dati, consulta l'articolo Monitorare la qualità tecnica della tua app con i vitali Android.

Identifica jank

Trovare il codice che causa il jank nella tua app può essere difficile. Questa sezione descrive tre metodi per identificare jank:

L'ispezione visiva ti consente di esaminare tutti i casi d'uso nella tua app in pochi minuti, ma non fornisce gli stessi dettagli di Systrace. Systrace fornisce ulteriori dettagli, ma se esegui Systrace per tutti i casi d'uso nella tua app, potresti essere sommerso da così tanti dati che possono essere difficili da analizzare. Sia l'ispezione visiva sia Systrace rilevano i jank sul tuo dispositivo locale. Se non riesci a riprodurre jank su dispositivi locali, puoi creare un monitoraggio personalizzato delle prestazioni per misurare parti specifiche della tua app su dispositivi in esecuzione sul campo.

Ispezione visiva

L'ispezione visiva ti aiuta a identificare i casi d'uso che producono jank. Per eseguire un'ispezione visiva, apri l'app, esamina manualmente le diverse parti dell'app e cerca jank nella UI.

Ecco alcuni suggerimenti per eseguire ispezioni visive:

  • Esegui una release della tua app, o almeno non di cui non è possibile eseguire il debug. Il runtime ART disattiva diverse ottimizzazioni importanti per supportare le funzionalità di debug, quindi assicurati di visualizzare qualcosa di simile a quello visualizzato da un utente.
  • Abilita il rendering GPU del profilo. Il processo di rendering GPU dei profili mostra delle barre sullo schermo che offrono una rappresentazione visiva del tempo necessario per il rendering dei frame di una finestra dell'interfaccia utente rispetto al benchmark di 16 ms per frame. Ogni barra ha componenti colorati mappati a una fase della pipeline di rendering, così puoi vedere quale parte richiede più tempo. Ad esempio, se il frame dedica molto tempo a gestire l'input, controlla il codice dell'app che gestisce l'input utente.
  • Esegui l'analisi dei componenti che sono origini comuni di jank, come RecyclerView.
  • Avvia l'app da avvio a freddo.
  • Esegui l'app su un dispositivo più lento per esacerbare il problema.

Quando trovi casi d'uso che producono jank, potresti avere un'idea della causa del jank nella tua app. Se hai bisogno di ulteriori informazioni, puoi utilizzare Systrace per indagare ulteriormente sulla causa.

Systrace

Sebbene Systrace sia uno strumento che mostra ciò che sta facendo l'intero dispositivo, può essere utile per identificare i blocchi nella tua app. L'overhead del sistema di Systrace è minimo, pertanto è possibile sperimentare incertezze realistiche durante la strumentazione.

Registra una traccia con Systrace durante il caso d'uso di scarsa qualità sul tuo dispositivo. Per istruzioni su come utilizzare Systrace, consulta Acquisizione di una traccia del sistema dalla riga di comando. Systrace è suddiviso per processi e thread. Cerca il processo dell'app in Systrace, simile alla figura 1.

Esempio di Systrace
Figura 1. Esempio di Systrace.

L'esempio di Systrace nella figura 1 contiene le seguenti informazioni per l'identificazione di jank:

  1. Systrace mostra quando viene disegnato ogni frame e ne codifica il colore per evidenziare tempi di rendering lenti. Questo ti aiuta a trovare singoli frame scadenti in modo più accurato rispetto all'ispezione visiva. Per maggiori informazioni, consulta Ispezionare frame e avvisi dell'interfaccia utente.
  2. Systrace rileva i problemi nell'app e mostra gli avvisi sia nei singoli frame che nel riquadro degli avvisi. È meglio seguire le istruzioni riportate nell'avviso.
  3. Parti del framework e delle librerie Android, come RecyclerView, contengono indicatori di traccia. Quindi, la sequenza temporale di systrace mostra quando questi metodi vengono eseguiti nel thread dell'interfaccia utente e il tempo necessario per l'esecuzione.

Dopo aver esaminato l'output di Systrace, nella tua app potrebbero esserci metodi che sospetti stiano causando jank. Ad esempio, se la sequenza temporale mostra che un frame lento è causato dal fatto che RecyclerView sta richiedendo molto tempo, puoi aggiungere eventi di traccia personalizzati al codice pertinente ed eseguire Systrace per ulteriori informazioni. Nel nuovo Systrace, la sequenza temporale mostra quando vengono chiamati i metodi dell'app e quanto tempo ne richiede l'esecuzione.

Se Systrace non mostra i dettagli sul motivo per cui il funzionamento del thread di UI sta richiedendo molto tempo, utilizza Android CPU Profiler per registrare una traccia del metodo campionata o strumentata. In genere, le tracce dei metodi non sono utili per identificare jank perché producono jank falsi positivi a causa dell'overhead elevato e non sono in grado di vedere quando i thread sono in esecuzione rispetto a quelli bloccati. Tuttavia, le tracce dei metodi possono aiutarti a identificare i metodi nella tua app che richiedono più tempo. Dopo aver identificato questi metodi, aggiungi gli indicatori di traccia ed esegui nuovamente Systrace per vedere se questi metodi causano jank.

Per ulteriori informazioni, consulta Comprendere Systrace.

Monitoraggio personalizzato del rendimento

Se non riesci a riprodurre jank su un dispositivo locale, puoi creare un monitoraggio personalizzato delle prestazioni nella tua app per identificare l'origine di jank sui dispositivi sul campo.

Per farlo, raccogli i tempi di rendering dei frame da parti specifiche della tua app con FrameMetricsAggregator e registra e analizza i dati utilizzando Firebase Performance Monitoring.

Per scoprire di più, consulta la guida introduttiva a Performance Monitoring per Android.

Frame bloccati

I frame bloccati sono frame dell'interfaccia utente che richiedono più di 700 ms per il rendering. Questo è un problema perché la tua app sembra essere bloccata e non risponde all'input utente per quasi un secondo intero durante il rendering del frame. Consigliamo di ottimizzare le app per eseguire il rendering di un frame entro 16 ms per garantire un'interfaccia utente fluida. Tuttavia, durante l'avvio dell'app o durante il passaggio a una schermata diversa, è normale che il fotogramma iniziale richieda più di 16 ms perché l'app deve aumentare le visualizzazioni, disporre lo schermo ed eseguire il disegno iniziale da zero. Ecco perché Android monitora i frame bloccati separatamente dal rendering lento. Nessun frame nella tua app dovrebbe mai richiedere più di 700 ms per il rendering.

Per aiutarti a migliorare la qualità dell'app, Android monitora automaticamente l'app per rilevare eventuali frame bloccati e mostra le informazioni nella dashboard Android Vitals. Per informazioni sulla modalità di raccolta dei dati, consulta l'articolo Monitorare la qualità tecnica dell'app con Android vitals.

I frame bloccati sono una forma estrema di rendering lento, quindi la procedura per diagnosticare e risolvere il problema è la stessa.

Monitoraggio jank

FrameTimeline in Perfetto può aiutarti a monitorare fotogrammi lenti o congelati.

Relazione tra frame lenti, frame bloccati e ANR

I frame lenti, i frame bloccati e gli ANR sono tutte diverse forme di jank che l'app potrebbe riscontrare. Per comprendere la differenza, consulta la tabella di seguito.

Frame lenti Frame bloccati ANR
Tempo di rendering Tra 16 ms e 700 ms Tra 700 ms e 5 s Più di 5 s
Area visibile dell'impatto sugli utenti
  • Scorrimento di RecyclerView che si comporta bruscamente
  • Su schermate con animazioni complesse che non vengono animate correttamente
  • Durante l'avvio dell'app
  • Passare da una schermata all'altra, ad esempio la transizione da una schermata all'altra
  • Mentre la tua attività è in primo piano, l'app non risponde a un evento di input o a BroadcastReceiver, ad esempio eventi di pressione di un tasto o tocco sullo schermo, entro cinque secondi.
  • Anche se non hai un'attività in primo piano, l'esecuzione del tuo BroadcastReceiver non è terminata per un periodo di tempo considerevole.

Monitorare separatamente i frame lenti e quelli bloccati

Durante l'avvio dell'app o durante il passaggio a una schermata diversa, è normale che il fotogramma iniziale richieda più di 16 ms perché l'app deve aumentare le visualizzazioni, disporre lo schermo ed eseguire l'estrazione iniziale da zero.

Best practice per dare priorità e risolvere i problemi di jank

Tieni presente le seguenti best practice quando cerchi di risolvere i problemi di jank nella tua app:

  • Identifica e risolvi le istanze di jank più facilmente riproducibili.
  • Dai la priorità agli errori ANR. Anche se i frame lenti o bloccati possono far apparire lenta un'app, gli errori ANR causano l'interruzione della risposta dell'app.
  • Il rendering lento è difficile da riprodurre, ma puoi iniziare abbandonando i frame bloccati da 700 ms. Questo si verifica quando l'app è in fase di avvio o di modifica delle schermate.

Correzione di jank

Per risolvere il problema di jank, controlla quali frame non vengono completati in 16 ms e cerca eventuali errori. Controlla se in alcuni frame Record View#draw o Layout impiegano troppo tempo in modo anomalo. Consulta le origini comuni di jank per questi problemi e altri.

Per evitare il jank, esegui le attività a lunga esecuzione in modo asincrono al di fuori del thread dell'interfaccia utente. Presta sempre attenzione al thread su cui è in esecuzione il tuo codice e presta attenzione quando pubblichi attività non banali nel thread principale.

Se hai un'interfaccia utente principale complessa e importante per la tua app, ad esempio l'elenco di scorrimento centrale, valuta la possibilità di scrivere test di strumentazione in grado di rilevare automaticamente tempi di rendering lenti ed eseguire spesso test per evitare regressioni.

Origini comuni di jank

Le seguenti sezioni illustrano le fonti comuni di jank nelle app che utilizzano il sistema View e le best practice per risolverli. Per informazioni sulla risoluzione dei problemi di prestazioni di Jetpack Compose, consulta Prestazioni di Jetpack Compose.

Elenchi scorrevoli

ListView, e in particolare RecyclerView, sono comunemente utilizzati per elenchi a scorrimento complessi più soggetti a jank. Entrambi contengono indicatori Systrace, quindi puoi utilizzare Systrace per vedere se stanno contribuendo a jank nella tua app. Passa l'argomento della riga di comando -a <your-package-name> per visualizzare le sezioni di traccia in RecyclerView, oltre agli eventuali indicatori di traccia che hai aggiunto. Se disponibili, segui le indicazioni degli avvisi generati nell'output di Systrace. In Systrace, puoi fare clic sulle sezioni tracciate in RecyclerView per visualizzare una spiegazione del lavoro svolto da RecyclerView.

RecyclerView: notificationDataSetChanged()

Se vedi che ogni elemento in RecyclerView viene restituito, quindi ridistribuito e ridisegnato in un unico frame, assicurati di non chiamare notifyDataSetChanged(), setAdapter(Adapter) o swapAdapter(Adapter, boolean) per piccoli aggiornamenti. Questi metodi indicano che sono state apportate modifiche all'intero contenuto dell'elenco e vengono visualizzati in Systrace come RV FullInvalidate. Utilizza invece SortedList o DiffUtil per generare aggiornamenti minimi quando i contenuti vengono modificati o aggiunti.

Prendiamo ad esempio un'app che riceve una nuova versione di un elenco di notizie da un server. Quando pubblichi queste informazioni sull'adattatore, puoi chiamare notifyDataSetChanged(), come mostrato nell'esempio seguente:

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

Lo svantaggio di questo è che se viene apportata una modifica banale, ad esempio un singolo elemento aggiunto nella parte superiore, RecyclerView non è a conoscenza. Di conseguenza, gli viene detto di eliminare l'intero stato dell'elemento memorizzato nella cache e di conseguenza deve riassociare tutto.

Ti consigliamo di utilizzare DiffUtil, che calcola e invia aggiornamenti minimi per te:

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

Per indicare a DiffUtil come ispezionare i tuoi elenchi, definisci MyCallback come implementazione di Callback.

RecyclerView: componenti RecyclerView nidificate

È comune nidificare più istanze di RecyclerView, soprattutto con un elenco verticale di elenchi a scorrimento orizzontale. Un esempio sono le griglie di app nella pagina principale del Play Store. Questa operazione può essere molto utile, ma vuol dire che ci sono anche molte visualizzazioni.

Se quando scorri la pagina verso il basso vedi molti elementi interni che si gonfiano, ti consigliamo di verificare se stai condividendo RecyclerView.RecycledViewPool tra istanze interne (orizzontali) di RecyclerView. Per impostazione predefinita, ogni elemento RecyclerView ha il proprio pool di elementi. Tuttavia, nel caso in cui sullo schermo sia visualizzata una dozzina di itemViews contemporaneamente, crea problemi quando itemViews non può essere condiviso dai diversi elenchi orizzontali se tutte le righe mostrano tipi di visualizzazioni simili.

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

Se vuoi eseguire un'ottimizzazione ulteriormente, puoi anche chiamare setInitialPrefetchItemCount(int) sul LinearLayoutManager della RecyclerView interna. Se, ad esempio, hai sempre 3,5 elementi visibili in una riga, chiama innerLLM.setInitialItemPrefetchCount(4). Questo segnala a RecyclerView che, quando una riga orizzontale sta per apparire sullo schermo, deve tentare di precaricare gli elementi al suo interno se c'è tempo nel thread dell'interfaccia utente.

RecyclerView: un'inflazione eccessiva o Create stanno richiedendo troppo tempo

Nella maggior parte dei casi, la funzionalità di precaricamento in RecyclerView può aiutarti a evitare il costo dell'aumento artificiale svolgendo le operazioni in anticipo quando il thread dell'interfaccia utente è inattivo. Se noti l'inflazione durante un frame e non nella sezione RV Prefetch, assicurati di eseguire il test su un dispositivo supportato e di utilizzare una versione recente della Support Library. Il precaricamento è supportato solo su livello API 21 di Android 5.0 e versioni successive.

Se noti spesso un aumento artificiale che causa il jank all'arrivo di nuovi elementi sullo schermo, verifica di non avere più tipi di visualizzazione del necessario. Minore è il numero di tipi di visualizzazione nei contenuti di un RecyclerView, minore è l'inflazione necessaria quando vengono visualizzati nuovi tipi di articoli sullo schermo. Se possibile, unisci i tipi di vista ove ragionevole. Se cambia solo un'icona, un colore o una porzione di testo da un tipo all'altro, puoi apportare questa modifica al momento del binding ed evitare l'inflazione, riducendo al contempo l'utilizzo di memoria dell'app.

Se i tuoi tipi di visualizzazione sembrano corretti, valuta la possibilità di ridurre il costo dell'inflazione. Può essere utile ridurre i container non necessari e le viste strutturali. Valuta la possibilità di creare itemViews con ConstraintLayout, che può aiutarti a ridurre le viste strutturali.

Se vuoi ottimizzare ulteriormente le prestazioni e le gerarchie di elementi sono semplici e non hai bisogno di funzionalità complesse per temi e stili, ti consigliamo di chiamare personalmente i costruttori. Tuttavia, spesso non vale la pena perdere la semplicità e le caratteristiche dell'XML.

RecyclerView: il processo di associazione richiede troppo tempo

L'associazione, ovvero onBindViewHolder(VH, int), deve essere diretta e richiedere molto meno di un millisecondo per tutto tranne gli elementi più complessi. Deve utilizzare gli elementi POJO (plain vecchio oggetto Java) dai dati degli elementi interni dell'adattatore e i setter di chiamate nelle viste nella ViewHolder. Se RV OnBindView sta richiedendo molto tempo, verifica che il tuo codice di associazione sia molto ridotto.

Se utilizzi oggetti POJO di base per conservare i dati nell'adattatore, puoi evitare del tutto di scrivere il codice di associazione in onBindViewHolder utilizzando la libreria di associazione dati.

RecyclerView o ListView: il layout o il disegno richiedono troppo tempo

Per problemi relativi a disegno e layout, consulta le sezioni Prestazioni del layout e Prestazioni del rendering.

Visualizzazione elenco: inflazione

Se non fai attenzione, puoi disattivare accidentalmente il riciclo in ListView. Se vedi un aumento artificiale ogni volta che appare un elemento sullo schermo, controlla che l'implementazione di Adapter.getView() sia in pausa, rivincola e restituisca il parametro convertView. Se l'implementazione di getView() aumenta sempre di più, la tua app non usufruirà dei vantaggi del riciclo in ListView. La struttura di getView() deve quasi sempre essere simile alla seguente implementazione:

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

Rendimento del layout

Se Systrace mostra che il segmento Layout di Choreographer#doFrame funziona troppo o troppo spesso, significa che stai riscontrando problemi di prestazioni del layout. Le prestazioni del layout dell'app dipendono dalla parte della gerarchia delle visualizzazioni con parametri o input di layout variabili.

Rendimento del layout: costo

Se i segmenti hanno una durata superiore a pochi millisecondi, è possibile che tu stia raggiungendo le prestazioni di annidamento peggiore per RelativeLayouts o weighted-LinearLayouts. Ciascuno di questi layout può attivare più passaggi di misurazione e layout dei propri elementi secondari, pertanto la loro nidificazione può portare a un comportamento di O(n^2) sulla profondità della nidificazione.

Prova a evitare RelativeLayout o la funzionalità di ponderazione di LinearLayout in tutti i nodi foglia tranne quelli più bassi della gerarchia. Ecco alcuni modi per farlo:

  • Riorganizzare le viste strutturali.
  • Definisci la logica di layout personalizzato. Per un esempio specifico, consulta Ottimizzare le gerarchie di layout. Puoi provare a eseguire la conversione a ConstraintLayout, che fornisce funzionalità simili, senza svantaggi in termini di prestazioni.

Rendimento del layout: frequenza

Il layout deve essere applicato quando vengono visualizzati nuovi contenuti sullo schermo, ad esempio quando un nuovo elemento scorre e viene reso visibile in RecyclerView. Se si verifica un layout significativo in ogni frame, è possibile che lo stai animando, con un'elevata probabilità di causare frame persi.

In genere, le animazioni devono essere eseguite su proprietà di disegno di View, ad esempio:

Puoi modificare tutte queste impostazioni in modo molto più economico rispetto alle proprietà del layout, come la spaziatura interna o i margini. In generale, è anche molto più economico modificare le proprietà di disegno di una vista chiamando un setter che attiva invalidate(), seguito da draw(Canvas) nel frame successivo. Questa operazione registra nuovamente le operazioni di disegno per la vista che è invalidata e in genere è molto più economica del layout.

Prestazioni di rendering

L'interfaccia utente di Android funziona in due fasi:

  • Record View#draw nel thread dell'interfaccia utente, che esegue draw(Canvas) su ogni vista invalidata e può richiamare le chiamate in viste personalizzate o nel codice.
  • DrawFrame sulla RenderThread, che viene eseguita sulla piattaforma RenderThread nativa, ma funziona in base al lavoro generato nella fase Record View#draw.

Prestazioni di rendering: thread UI

Se la visualizzazione record#draw sta richiedendo molto tempo, è comune che una bitmap venga visualizzata nel thread dell'interfaccia utente. La colorazione a una bitmap utilizza il rendering della CPU, quindi generalmente evita questa situazione quando possibile. Puoi utilizzare il tracciamento del metodo con Android CPU Profiler per vedere se questo è il problema.

La colorazione a una bitmap viene spesso eseguita quando un'app vuole decorare una bitmap prima di visualizzarla. A volte si usa una decorazione come l'aggiunta di angoli arrotondati:

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

Se questo è il tipo di lavoro che stai svolgendo nel thread dell'interfaccia utente, puoi farlo nel thread di decodifica in background. In alcuni casi, come nell'esempio precedente, puoi anche lavorare al momento di disegnare. Quindi, se il codice Drawable o View ha un aspetto simile al seguente:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

Puoi sostituirlo con:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

Puoi eseguire questa operazione anche per proteggere lo sfondo, ad esempio per disegnare un gradiente sopra la bitmap e applicare filtri alle immagini con ColorMatrixColorFilter, due altre operazioni comuni che consentono di modificare le bitmap.

Se stai disegnando su una bitmap per un altro motivo, magari come cache, prova a attingere al valore Canvas con accelerazione hardware che viene passato direttamente al tuo View o Drawable. Se necessario, valuta anche di chiamare setLayerType() con LAYER_TYPE_HARDWARE per memorizzare nella cache l'output del rendering complesso e sfruttare comunque il rendering della GPU.

Prestazioni di rendering: RenderThread

Alcune operazioni Canvas sono economiche da registrare, ma comportano calcoli costosi sul RenderThread. In genere Systrace li spiega con degli avvisi.

Animazione di percorsi di grandi dimensioni

Quando viene chiamato Canvas.drawPath() per il Canvas con accelerazione hardware passato a View, Android disegna questi percorsi per primi sulla CPU e li carica nella GPU. Se hai percorsi di grandi dimensioni, evita di modificarli da un frame all'altro, in modo da poterli memorizzare nella cache e disegnarli in modo efficiente. drawPoints(), drawLines() e drawRect/Circle/Oval/RoundRect() sono più efficienti e migliori da utilizzare anche se utilizzi più chiamate di disegno.

Canvas.clipPath

clipPath(Path) attiva un comportamento di ritaglio costoso e in genere deve essere evitato. Quando possibile, opta per disegnare forme anziché ritagliare a rettangoli. Ha un rendimento migliore e supporta l'anti-aliasing. Ad esempio, la seguente chiamata clipPath può essere espressa in modo diverso:

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

Esprimi invece l'esempio precedente come segue:

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
Caricamenti bitmap

Android mostra le bitmap come texture OpenGL e la prima volta che una bitmap viene visualizzata in un frame, viene caricata nella GPU. In Systrace puoi vedere questo valore in Caricamento trama(ID) larghezza x altezza. Questa operazione può richiedere diversi millisecondi, come mostrato nella Figura 2, ma è necessario per visualizzare l'immagine con la GPU.

Se questi richiedono molto tempo, controlla prima i numeri di larghezza e altezza nella traccia. Assicurati che la bitmap visualizzata non sia molto più grande dell'area sullo schermo in cui viene visualizzata. Se lo è, il caricamento e la memoria potrebbero sprecare. In genere, le librerie di caricamento bitmap forniscono un mezzo per richiedere una bitmap di dimensioni appropriate.

In Android 7.0, il codice di caricamento bitmap, generalmente eseguito dalle librerie, può chiamare prepareToDraw() per attivare un caricamento anticipato prima che sia necessario. In questo modo, il caricamento avviene in anticipo quando RenderThread è inattivo. Puoi eseguire questa operazione dopo la decodifica o l'associazione di una bitmap a una vista, a condizione che tu conosca la bitmap. Idealmente, questa operazione viene eseguita automaticamente dalla tua libreria di caricamento bitmap, ma se gestisci la tua libreria o vuoi assicurarti di non inviare i caricamenti sui dispositivi più recenti, puoi chiamare prepareToDraw() nel tuo codice.

Un&#39;app trascorre molto tempo in un frame caricando una bitmap di grandi dimensioni
Figura 2. Un'app trascorre molto tempo in un frame caricando una bitmap di grandi dimensioni. Riduci le dimensioni o attivalo in anticipo quando lo decodificherai con prepareToDraw().

Ritardi nella pianificazione dei thread

Lo scheduler dei thread è la parte del sistema operativo Android che deve decidere quali thread nel sistema devono essere eseguiti, quando devono essere eseguiti e per quanto tempo.

A volte, il jank si verifica perché il thread dell'interfaccia utente dell'app è bloccato o non è in esecuzione. Systrace utilizza colori diversi, come mostrato nella Figura 3, per indicare quando un thread è inattiva (grigio), eseguibile (blu: può essere eseguito, ma non è ancora stato selezionato dallo scheduler per l'esecuzione), attivamente in esecuzione (verde) o in modalità di sospensione senza interruzioni (rosso o arancione). Questa funzionalità è estremamente utile per il debug dei problemi di jank causati da ritardi nella pianificazione dei thread.

Evidenzia un periodo in cui il thread dell&#39;interfaccia utente è sospeso
Figura 3. Evidenzia un periodo in cui il thread dell'interfaccia utente è sospeso.

Spesso, le chiamate a Binder, il meccanismo di comunicazione tra processi (IPC) su Android, causano lunghe pause nell'esecuzione dell'app. Nelle versioni successive di Android, è uno dei motivi più comuni per l'interruzione dell'esecuzione del thread dell'interfaccia utente. In genere, la soluzione è evitare di chiamare funzioni che effettuano chiamate a Binder. Se è inevitabile, memorizza il valore nella cache o sposta il lavoro nei thread in background. Se i codebase diventano più grandi, se non fai attenzione puoi aggiungere accidentalmente una chiamata binder richiamando un metodo di basso livello. Tuttavia, puoi trovarli e correggerli con il tracciamento.

Se disponi di transazioni binder, puoi acquisire i relativi stack di chiamate con i seguenti comandi adb:

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

A volte le chiamate che sembrano innocue, come getRefreshRate(), possono attivare transazioni di Binder e causare gravi problemi quando vengono chiamate di frequente. La traccia periodica può aiutarti a individuare e risolvere i problemi che si presentano.

Mostra il thread di UI in stato di sospensione a causa delle transazioni di Binder
  in un camper. Mantieni focalizzata la logica di bind e utilizza tracker-ipc per individuare e rimuovere le chiamate di binder.
Figura 4. Il thread dell'interfaccia utente non è attivo a causa delle transazioni di Binder in un camper. Mantieni la logica di bind semplice e utilizza trace-ipc per individuare e rimuovere le chiamate a Binder.

Se non vedi l'attività di Bider, ma non riesci ancora a vedere il tuo thread di UI in esecuzione, assicurati di non essere in attesa di un blocco o di un'altra operazione da un altro thread. In genere, il thread dell'interfaccia utente non deve attendere i risultati di altri thread. Gli altri thread devono pubblicare informazioni al loro interno.

Allocazione di oggetti e garbage collection

L'allocazione degli oggetti e la garbage collection (GC) rappresentano un problema molto minore da quando ART è stato introdotto come runtime predefinito in Android 5.0, ma è comunque possibile appesantire i thread con questo lavoro aggiuntivo. È possibile allocare in risposta a un evento raro che non si verifica molte volte al secondo, ad esempio un utente che tocca un pulsante, ma ricorda che ogni allocazione ha un costo. Se si trova in un ciclo chiuso chiamato di frequente, valuta la possibilità di evitare l'allocazione per alleggerire il carico su GC.

Systrace ti mostra se GC è in esecuzione frequentemente e Android Memory Profiler può mostrarti la provenienza delle allocazioni. Se, quando possibile, eviti le allocazioni, soprattutto in presenza di loop stretti, si riducono i problemi.

Mostra un GC di 94 ms sull&#39;HeapTaskDaemon
Figura 5. Un GC di 94 ms sul thread HeapTaskDaemon.

Sulle versioni recenti di Android, GC in genere viene eseguito su un thread in background denominato HeapTaskDaemon. Una quantità significativa di allocazione può comportare l'utilizzo di una quantità maggiore di risorse della CPU per GC, come mostrato nella Figura 5.