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.
L'esempio di Systrace nella figura 1 contiene le seguenti informazioni per identificare i problemi di jank:
- 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.
- 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.
- 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 |
|
|
|
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 suRenderThreadnativo, 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.
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.
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.
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.
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.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Eseguire il benchmark dell'app
- Panoramica della misurazione delle prestazioni dell'app
- Best practice per l'ottimizzazione delle app