Rendering lento

Il rendering dell'interfaccia utente è l'azione di generare un frame dalla tua app e visualizzarlo sullo schermo. Per garantire un'interazione fluida dell'utente con la tua app, la tua app deve eseguire il rendering dei frame in meno di 16 ms per raggiungere i 60 frame al secondo (fps). Per capire perché sono preferiti 60 fps, consulta Android Performance Patterns: Why 60fps?. Se stai cercando di raggiungere i 90 fps, questa finestra scende a 11 ms e per 120 fps è di 8 ms.

Se superi questa finestra di 1 ms, non significa che il frame viene visualizzato 1 ms in ritardo, ma Choreographer il frame viene eliminato completamente. Se la tua app presenta un rendering lento dell'interfaccia utente, il sistema è costretto a saltare i frame e l'utente percepisce un'interruzione nella tua app. Questo fenomeno è chiamato jank. Questa pagina mostra come diagnosticare e risolvere i problemi di jank.

Se sviluppi giochi che non utilizzano il sistema View, ignori Choreographer. In questo caso, la libreria Frame Pacing aiuta i giochi OpenGL e Vulkan a ottenere un rendering fluido e un corretto pacing dei frame su Android.

Identificare il jank

Trovare il codice nell'app che causa jank può essere difficile. Questa sezione descrive tre metodi per identificare i problemi di jank:

L'ispezione visiva ti consente di esaminare tutti i casi d'uso della tua app in pochi minuti, ma non fornisce tanti dettagli quanto Systrace. Systrace fornisce più dettagli, ma se lo esegui per tutti i casi d'uso della tua app, puoi essere inondato da così tanti dati che possono essere difficili da analizzare. Sia l'ispezione visiva sia Systrace rilevano i problemi di jank sul tuo dispositivo locale. Se non riesci a riprodurre jank sui dispositivi locali, puoi creare un monitoraggio personalizzato delle prestazioni per misurare parti specifiche della tua app sui 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 e scorri manualmente le diverse parti dell'app per individuare eventuali problemi di jank nell'interfaccia utente.

Ecco alcuni suggerimenti per eseguire ispezioni visive:

  • Esegui una versione di rilascio o almeno non eseguibile in modalità di debug della tua app. L'ambiente di runtime ART disattiva diverse ottimizzazioni importanti per supportare le funzionalità di debug, quindi assicurati di esaminare qualcosa di simile a ciò che vede un utente.
  • Attiva il rendering GPU del profilo. Il rendering GPU del profilo mostra sullo schermo delle barre che forniscono una rappresentazione visiva del tempo necessario per il rendering dei frame di una finestra della UI rispetto al benchmark di 16 ms per frame. Ogni barra ha componenti colorati che corrispondono a una fase della pipeline di rendering, in modo da poter vedere quale parte richiede più tempo. Ad esempio, se il frame trascorre molto tempo a gestire l'input, esamina il codice dell'app che gestisce l'input utente.
  • Esamina i componenti che sono fonti comuni di jank, ad esempio RecyclerView.
  • Avvia l'app da un 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 una buona idea di cosa sta causando il jank nella tua app. Se hai bisogno di maggiori informazioni, puoi utilizzare Systrace per esaminare ulteriormente la causa.

Systrace

Sebbene Systrace sia uno strumento che mostra cosa sta facendo l'intero dispositivo, può essere utile per identificare i problemi di jank nella tua app. Systrace ha un sovraccarico di sistema minimo, quindi puoi riscontrare problemi di jank realistici durante l'instrumentation.

Registra una traccia con Systrace mentre esegui lo scenario d'uso con problemi di prestazioni sul tuo dispositivo. Per istruzioni su come utilizzare Systrace, vedi Acquisire una traccia di sistema dalla riga di comando. Systrace è suddiviso per processi e thread. Cerca il processo della tua app in Systrace, che ha un aspetto simile a quello della figura 1.

Esempio di Systrace
Figura 1. Esempio di Systrace.

L'esempio di Systrace nella figura 1 contiene le seguenti informazioni per identificare i problemi di jank:

  1. Systrace mostra quando viene disegnato ogni frame e codifica a colori ogni frame per evidenziare i tempi di rendering lenti. In questo modo, puoi trovare i singoli frame instabili in modo più accurato rispetto all'ispezione visiva. Per saperne di più, vedi Ispezionare i frame e gli avvisi dell'interfaccia utente.
  2. Systrace rileva i problemi nella tua app e mostra avvisi sia nei singoli frame sia nel pannello Avvisi. Ti consigliamo di seguire le indicazioni riportate nell'avviso.
  3. Parti del framework e delle librerie Android, ad esempio RecyclerView, contengono marcatori di traccia. Pertanto, la cronologia di systrace mostra quando questi metodi vengono eseguiti sul thread UI e quanto tempo impiegano per essere eseguiti.

Dopo aver esaminato l'output di Systrace, potrebbero esserci metodi nella tua app che sospetti causino jank. Ad esempio, se la sequenza temporale mostra che un frame lento è causato da RecyclerView che richiede molto tempo, puoi aggiungere eventi di traccia personalizzati al codice pertinente e rieseguire Systrace per ulteriori informazioni. Nel nuovo Systrace, la sequenza temporale mostra quando vengono chiamati i metodi della tua app e quanto tempo impiegano per essere eseguiti.

Se Systrace non mostra i dettagli sul motivo per cui il lavoro del thread dell'interfaccia utente richiede molto tempo, utilizza Android CPU Profiler per registrare una traccia metodo campionata o strumentata. In genere, le tracce dei metodi non sono adatte a identificare i problemi di jank perché producono jank falsi positivi a causa di un overhead elevato e non riescono a vedere quando i thread sono in esecuzione rispetto a quando sono bloccati. Tuttavia, le tracce dei metodi possono aiutarti a identificare i metodi della tua app che richiedono più tempo. Dopo aver identificato questi metodi, aggiungi i marcatori di traccia ed esegui nuovamente Systrace per verificare se questi metodi causano jank.

Per saperne di più, vedi Informazioni su Systrace.

Monitoraggio delle prestazioni personalizzato

Se non riesci a riprodurre il jank su un dispositivo locale, puoi integrare il monitoraggio personalizzato delle prestazioni nella tua app per identificare l'origine del 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 saperne 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. Si tratta di un problema perché la tua app sembra bloccata e non risponde all'input dell'utente per quasi un secondo intero durante il rendering del frame. Ti consigliamo di ottimizzare le app per il rendering di un frame entro 16 ms per garantire un'interfaccia utente fluida. Tuttavia, durante l'avvio dell'app o il passaggio a una schermata diversa, è normale che il frame iniziale impieghi più di 16 ms per essere disegnato perché l'app deve gonfiare le visualizzazioni, disporre la schermata ed eseguire il disegno iniziale da zero. Per questo motivo, Android tiene traccia dei frame bloccati separatamente dal rendering lento. Nessun frame nella tua app dovrebbe richiedere più di 700 ms per il rendering.

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

Scatti nel monitoraggio

FrameTimeline in Perfetto può aiutarti a monitorare i frame lenti o bloccati.

Relazione tra frame lenti, frame bloccati e ANR

I frame lenti, i frame bloccati e gli ANR sono tutte forme diverse di jank che la tua app potrebbe riscontrare. Consulta la tabella seguente per capire la differenza.

Frame lenti Frame bloccati ANR
Tempo di rendering Tra 16 ms e 700 ms Tra 700 ms e 5 s Più di 5 secondi
Area di impatto visibile per l'utente
  • RecyclerView scorrimento che si interrompe bruscamente
  • Su schermi con animazioni complesse che non vengono animate correttamente
  • Durante l'avvio dell'app
  • Passaggio da una schermata all'altra, ad esempio la transizione delle schermate
  • Mentre l'attività è in primo piano, l'app non ha risposto a un evento di input o BroadcastReceiver, ad esempio pressione di tasti o tocchi dello schermo, entro cinque secondi.
  • Mentre non hai un'attività in primo piano, l'esecuzione di BroadcastReceiver non è terminata in un periodo di tempo considerevole.

Monitorare separatamente i frame lenti e i frame bloccati

Durante l'avvio dell'app o il passaggio a una schermata diversa, è normale che il frame iniziale impieghi più di 16 ms per essere disegnato perché l'app deve gonfiare le visualizzazioni, disporre la schermata ed eseguire il disegno iniziale da zero.

Best practice per dare la priorità ai problemi di jank e risolverli

Quando cerchi di risolvere i problemi di scattosità nella tua app, tieni presente le seguenti best practice:

  • Identifica e risolvi le istanze di jank più facilmente riproducibili.
  • Dai la priorità agli errori ANR. Mentre i frame lenti o bloccati potrebbero far sembrare un'app lenta, gli errori ANR causano l'interruzione della risposta dell'app.
  • Il rendering lento è difficile da riprodurre, ma puoi iniziare eliminando i frame bloccati per 700 ms. Questo problema si verifica più spesso durante l'avvio dell'app o il cambio di schermata.

Correzione del jank

Per risolvere il problema, controlla quali frame non vengono completati in 16 ms e cerca l'errore. Controlla se Record View#draw o Layout impiegano un tempo insolitamente lungo in alcuni frame. Per questi e altri problemi, consulta Fonti comuni di jank.

Per evitare jank, esegui le attività di lunga durata in modo asincrono al di fuori del thread dell'interfaccia utente. Tieni sempre presente il thread su cui viene eseguito il codice e fai attenzione quando pubblichi attività non banali nel thread principale.

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

Fonti comuni di jank

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

Elenchi scorrevoli

ListView e soprattutto RecyclerView sono di uso comune per elenchi di scorrimento complessi più suscettibili a problemi di jank. Entrambi contengono indicatori Systrace, quindi puoi utilizzare Systrace per verificare se contribuiscono a problemi di jank nella tua app. Passa l'argomento della riga di comando -a <your-package-name> per visualizzare le sezioni di traccia in RecyclerView, nonché gli 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 RecyclerView-tracciate per visualizzare una spiegazione del lavoro svolto da RecyclerView.

RecyclerView: notifyDataSetChanged()

Se vedi che ogni elemento del tuo RecyclerView viene riassociato e quindi riorganizzato e ridisegnato in un unico frame, assicurati di non chiamare notifyDataSetChanged(), setAdapter(Adapter) o swapAdapter(Adapter, boolean) per piccoli aggiornamenti. Questi metodi segnalano 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.

Ad esempio, considera un'app che riceve una nuova versione di un elenco di contenuti di notizie da un server. Quando pubblichi queste informazioni nell'adattatore, è possibile 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 è che se viene apportata una modifica banale, ad esempio l'aggiunta di un singolo elemento in cima, RecyclerView non ne è a conoscenza. Pertanto, viene chiesto di eliminare l'intero stato dell'elemento memorizzato nella cache e quindi è necessario riassociare tutto.

Ti consigliamo di utilizzare DiffUtil, che calcola e invia gli 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 informare DiffUtil su come ispezionare i tuoi elenchi, definisci MyCallback come implementazione di Callback.

RecyclerView: RecyclerView nidificati

È 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. Può funzionare alla grande, ma ci sono anche molte visualizzazioni che si muovono.

Se noti che molti elementi interni si espandono quando scorri la pagina verso il basso, ti consigliamo di verificare di condividere RecyclerView.RecycledViewPool tra le istanze interne (orizzontali) di RecyclerView. Per impostazione predefinita, ogni RecyclerView ha il proprio pool di elementi. Tuttavia, nel caso di una dozzina di itemViews sullo schermo contemporaneamente, è problematico 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 ottimizzare ulteriormente, puoi anche chiamare setInitialPrefetchItemCount(int) sul LinearLayoutManager del RecyclerView interno. Se, ad esempio, hai sempre 3,5 elementi visibili in una riga, chiama innerLLM.setInitialItemPrefetchCount(4). Questo indica a RecyclerView che quando una riga orizzontale sta per apparire sullo schermo, deve tentare di precaricare gli elementi al suo interno se c'è tempo libero nel thread UI.

RecyclerView: troppa inflazione o Create richiede troppo tempo

Nella maggior parte dei casi, la funzionalità di precaricamento in RecyclerView può contribuire a ovviare al costo dell'inflate eseguendo il lavoro in anticipo mentre il thread dell'interfaccia utente è inattivo. Se noti un aumento dell'inflazione durante un frame e non in una sezione etichettata 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 Android 5.0 livello API 21 e versioni successive.

Se noti spesso un aumento dell'inflazione che causa problemi di riproduzione quando vengono visualizzati 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 da applicare quando sullo schermo vengono visualizzati nuovi tipi di elementi. Se possibile, unisci i tipi di visualizzazione quando è ragionevole. Se tra i tipi cambia solo un'icona, un colore o un testo, puoi apportare la modifica al momento del binding ed evitare l'inflazione, riducendo al contempo l'impronta di memoria dell'app.

Se i tipi di visualizzazione ti soddisfano, valuta la possibilità di ridurre il costo dell'inflazione. Ridurre le visualizzazioni non necessarie di contenitori e strutture può essere utile. Valuta la possibilità di creare itemViews con ConstraintLayout, che possono contribuire a ridurre le visualizzazioni strutturali.

Se vuoi ottimizzare ulteriormente il rendimento e le gerarchie degli elementi sono semplici e non hai bisogno di funzionalità complesse di temi e stili, ti consigliamo di chiamare direttamente i costruttori. Tuttavia, spesso non vale la pena rinunciare alla semplicità e alle funzionalità di XML.

RecyclerView: Bind taking too long

Il binding, ovvero onBindViewHolder(VH, int), deve essere semplice e richiedere molto meno di un millisecondo per tutti gli elementi tranne quelli più complessi. Deve prendere elementi POJO (Plain Old Java Object) dai dati degli articoli interni dell'adattatore e chiamare i setter nelle visualizzazioni in ViewHolder. Se RV OnBindView richiede molto tempo, verifica di eseguire un lavoro minimo nel codice di binding.

Se utilizzi oggetti POJO di base per archiviare i dati nell'adattatore, puoi evitare completamente di scrivere il codice di binding in onBindViewHolder utilizzando la libreria Data Binding.

RecyclerView o ListView: layout o disegno che richiedono troppo tempo

Per problemi con il disegno e il layout, consulta le sezioni Prestazioni del layout e Prestazioni di rendering.

ListView: Inflation

Se non fai attenzione, puoi disattivare accidentalmente il riciclo in ListView. Se visualizzi l'inflazione ogni volta che un elemento viene visualizzato sullo schermo, verifica che l'implementazione di Adapter.getView() stia eseguendo la musing, il re-binding e restituendo il parametro convertView. Se la tua implementazione di getView() esegue sempre l'inflazione, la tua app non usufruisce dei vantaggi del riciclo in ListView. La struttura del tuo 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 rendimento del layout. Il rendimento del layout della tua app dipende dalla porzione della gerarchia di oggetti View che ha parametri o input di layout variabili.

Rendimento del layout: costo

Se i segmenti durano più di qualche millisecondo, è possibile che tu stia riscontrando le prestazioni di nidificazione nel caso peggiore per RelativeLayouts o weighted-LinearLayouts. Ciascuno di questi layout può attivare più passaggi di misurazione e layout dei relativi elementi secondari, pertanto il loro annidamento può comportare un comportamento O(n^2) in base alla profondità dell'annidamento.

Cerca di evitare RelativeLayout o la funzionalità di peso diLinearLayout in tutti i nodi, tranne in quelli foglia più bassi della gerarchia. Ecco alcuni modi per farlo:

  • Riorganizza le visualizzazioni strutturali.
  • Definisci la logica del layout personalizzato. Per un esempio specifico, consulta la sezione Ottimizzare le gerarchie di layout. Puoi provare a eseguire la conversione a ConstraintLayout, che offre funzionalità simili, senza gli svantaggi in termini di rendimento.

Rendimento del layout: frequenza

Il layout è previsto quando nuovi contenuti vengono visualizzati sullo schermo, ad esempio quando un nuovo elemento scorre nella visualizzazione in RecyclerView. Se si verifica un layout significativo su ogni fotogramma, è possibile che tu stia animando il layout, il che probabilmente causerà la perdita di fotogrammi.

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

Puoi modificarli tutti a un costo molto inferiore rispetto alle proprietà di layout, come il padding o i margini. In genere, è anche molto più economico modificare le proprietà di disegno di una vista chiamando un setter che attiva un invalidate(), seguito da draw(Canvas) nel frame successivo. Questa operazione registra nuovamente le operazioni di disegno per la visualizzazione invalidata ed è in genere molto più economica del layout.

Prestazioni di rendering

L'interfaccia utente di Android funziona in due fasi:

  • Record View#draw sul thread dell'interfaccia utente, che viene eseguito draw(Canvas) su ogni visualizzazione invalidata e può richiamare chiamate in visualizzazioni personalizzate o nel tuo codice.
  • DrawFrame su RenderThread, che viene eseguito su RenderThread nativo, ma funziona in base al lavoro generato dalla fase Record View#draw.

Prestazioni di rendering: thread UI

Se Record View#draw richiede molto tempo, è normale che una bitmap venga disegnata sul thread dell'interfaccia utente. Il rendering su una bitmap utilizza il rendering della CPU, quindi in genere evita questa operazione quando possibile. Puoi utilizzare la traccia dei metodi con Android CPU Profiler per verificare se questo è il problema.

Il disegno su una bitmap viene spesso eseguito quando un'app vuole decorare una bitmap prima di visualizzarla, ad esempio aggiungendo 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 sul thread dell'interfaccia utente, puoi farlo sul thread di decodifica in background. In alcuni casi, come nell'esempio precedente, puoi persino eseguire il lavoro al momento del disegno. Quindi, se il codice Drawable o View è 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 questo:

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 farlo anche per la protezione dello sfondo, ad esempio quando disegni un gradiente sopra la bitmap e per il filtraggio delle immagini con ColorMatrixColorFilter, due altre operazioni comuni eseguite modificando le bitmap.

Se disegni su una bitmap per un altro motivo, ad esempio per utilizzarla come cache, prova a disegnare direttamente sulla Canvas con accelerazione hardware passata a View o Drawable. Se necessario, valuta anche la possibilità di chiamare setLayerType() con LAYER_TYPE_HARDWARE per memorizzare nella cache l'output di rendering complesso e sfruttare comunque il rendering della GPU.

Prestazioni di rendering: RenderThread

Alcune operazioni Canvas sono economiche da registrare, ma attivano calcoli costosi su RenderThread. Systrace in genere li segnala con avvisi.

Animare percorsi di grandi dimensioni

Quando viene chiamato Canvas.drawPath() sull'acceleratore hardware Canvas passato a View, Android disegna prima questi percorsi sulla CPU e li carica sulla GPU. Se hai tracciati di grandi dimensioni, evita di modificarli fotogramma per fotogramma, in modo che possano essere memorizzati nella cache e disegnati in modo efficiente. drawPoints(), drawLines() e drawRect/Circle/Oval/RoundRect() sono più efficienti e migliori da usare anche se utilizzi più chiamate di disegno.

Canvas.clipPath

clipPath(Path) attiva un comportamento di ritaglio costoso e deve essere generalmente evitato. Se possibile, scegli di disegnare forme anziché ritagliare in forme non rettangolari. Funziona meglio 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 nel seguente modo:

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 di bitmap

Android visualizza le bitmap come texture OpenGL e la prima volta che una bitmap viene visualizzata in un frame, viene caricata sulla GPU. Puoi visualizzarlo in Systrace come Texture upload(id) width x height. L'operazione può richiedere diversi millisecondi, come mostrato nella figura 2, ma è necessaria per visualizzare l'immagine con la GPU.

Se queste operazioni richiedono molto tempo, controlla innanzitutto i numeri di larghezza e altezza nella traccia. Assicurati che la bitmap visualizzata non sia molto più grande dell'area dello schermo in cui viene mostrata. In caso affermativo, si sprecano tempo di caricamento e memoria. In genere, le librerie di caricamento delle bitmap forniscono un mezzo per richiedere una bitmap di dimensioni appropriate.

In Android 7.0, il codice di caricamento delle 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 mentre RenderThread è inattivo. Puoi farlo dopo la decodifica o durante l'associazione di una bitmap a una visualizzazione, a condizione che tu conosca la bitmap. Idealmente, la libreria di caricamento bitmap lo fa per te, ma se gestisci la tua o vuoi assicurarti di non raggiungere i limiti di caricamento sui dispositivi più recenti, puoi chiamare prepareToDraw() nel tuo codice.

Un'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 decodifichi con prepareToDraw().

Ritardi nella pianificazione dei thread

Lo scheduler dei thread è la parte del sistema operativo Android incaricata di decidere quali thread del sistema devono essere eseguiti, quando vengono eseguiti e per quanto tempo.

A volte, i problemi di jank si verificano perché il thread UI dell'app è bloccato o non è in esecuzione. Systrace utilizza colori diversi, come mostrato nella figura 3, per indicare quando un thread è inattivo (grigio), eseguibile (blu: può essere eseguito, ma non è ancora stato selezionato dallo scheduler), in esecuzione attiva (verde) o in sospensione non interrompibile (rosso o arancione). Ciò è estremamente utile per il debug dei problemi di jank causati da ritardi nella pianificazione dei thread.

Evidenzia un periodo in cui il thread dell'interfaccia utente
  è inattivo
Figura 3. Evidenziazione di un periodo in cui il thread dell'interfaccia utente è inattivo.

Spesso, le chiamate binder, il meccanismo di comunicazione interprocesso (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 correzione consiste nell'evitare di chiamare funzioni che effettuano chiamate del binder. Se è inevitabile, memorizza nella cache il valore o sposta il lavoro nei thread in background. Man mano che 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 risolverli con la tracciabilità.

Se hai transazioni del raccoglitore, puoi acquisire i relativi call stack 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 chiamate che sembrano innocue, come getRefreshRate(), possono attivare transazioni di binder e causare grossi problemi se vengono chiamate di frequente. Il tracciamento periodico può aiutarti a trovare e risolvere questi problemi man mano che si presentano.

Mostra il thread UI inattivo a causa di transazioni binder in uno scorrimento RV. Mantieni la logica di binding mirata e utilizza trace-ipc per
  individuare e rimuovere le chiamate binder.
Figura 4. Il thread UI è inattivo a causa di transazioni binder in uno scorrimento rapido della RV. Mantieni semplice la logica di binding e utilizza trace-ipc per individuare e rimuovere le chiamate del binder.

Se non vedi l'attività del binder, ma ancora non vedi l'esecuzione del thread dell'interfaccia utente, 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.

Allocazione di oggetti e garbage collection

L'allocazione di oggetti e la garbage collection (GC) sono un problema molto meno rilevante da quando ART è stato introdotto come runtime predefinito in Android 5.0, ma è ancora 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 stretto chiamato di frequente, valuta la possibilità di evitare l'allocazione per alleggerire il carico sul GC.

Systrace mostra se GC viene eseguito di frequente e Android Memory Profiler può mostrare da dove provengono le allocazioni. Se eviti le allocazioni quando possibile, soprattutto nei cicli stretti, è meno probabile che si verifichino problemi.

Mostra una GC di 94 ms su HeapTaskDaemon
Figura 5. Una GC di 94 ms sul thread HeapTaskDaemon.

Nelle versioni recenti di Android, GC viene generalmente eseguito su un thread in background denominato HeapTaskDaemon. Quantità significative di allocazione possono comportare una maggiore spesa di risorse della CPU per la GC, come mostrato nella Figura 5.