Suggerimenti e strumenti per l'ottimizzazione delle app Android

Pubblicato: 2022-03-11

I dispositivi Android hanno molti core, quindi scrivere app fluide è un compito semplice per chiunque, giusto? Sbagliato. Poiché tutto su Android può essere fatto in molti modi diversi, scegliere l'opzione migliore può essere difficile. Se vuoi scegliere il metodo più efficiente, devi sapere cosa sta succedendo sotto il cofano. Fortunatamente, non devi fare affidamento sui tuoi sentimenti o sull'olfatto, poiché ci sono molti strumenti là fuori che possono aiutarti a trovare i colli di bottiglia misurando e descrivendo cosa sta succedendo. Le app correttamente ottimizzate e fluide migliorano notevolmente l'esperienza dell'utente e consumano anche meno batteria.

Vediamo prima alcuni numeri per considerare quanto sia importante l'ottimizzazione. Secondo un post di Nimbledroid, l'86% degli utenti (me compreso) ha disinstallato le app dopo averle utilizzate solo una volta a causa delle scarse prestazioni. Se stai caricando del contenuto, hai meno di 11 secondi per mostrarlo all'utente. Solo un utente su tre ti darà più tempo. Potresti anche ricevere molte recensioni negative su Google Play a causa di ciò.

Crea app migliori: modelli di prestazioni Android

Mettere alla prova la pazienza degli utenti è una scorciatoia per la disinstallazione.
Twitta

La prima cosa che ogni utente nota più e più volte è il tempo di avvio dell'app. Secondo un altro post di Nimbledroid, delle 100 migliori app, 40 si avviano in meno di 2 secondi e 70 in meno di 3 secondi. Quindi, se possibile, dovresti generalmente visualizzare alcuni contenuti il ​​prima possibile e ritardare un po' i controlli in background e gli aggiornamenti.

Ricorda sempre che l'ottimizzazione prematura è la radice di tutti i mali. Inoltre, non dovresti perdere troppo tempo con la micro ottimizzazione. Vedrai il massimo vantaggio dell'ottimizzazione del codice che viene eseguito spesso. Ad esempio, questo include la funzione onDraw() , che esegue ogni fotogramma, idealmente 60 volte al secondo. Il disegno è l'operazione più lenta in circolazione, quindi prova a ridisegnare solo ciò che devi. Maggiori informazioni su questo verranno dopo.

Suggerimenti per le prestazioni

Abbastanza teoria, ecco un elenco di alcune delle cose che dovresti considerare se le prestazioni sono importanti per te.

1. String contro StringBuilder

Diciamo che hai una stringa e per qualche motivo vuoi aggiungervi più stringhe 10 mila volte. Il codice potrebbe assomigliare a questo.

 String string = "hello"; for (int i = 0; i < 10000; i++) { string += " world"; }

Puoi vedere sui monitor Android Studio quanto inefficiente può essere una concatenazione di stringhe. Ci sono tonnellate di Garbage Collection (GC) in corso.

Stringa contro StringBuilder

Questa operazione richiede circa 8 secondi sul mio dispositivo abbastanza buono, che ha Android 5.1.1. Il modo più efficiente per raggiungere lo stesso obiettivo è usare uno StringBuilder, come questo.

 StringBuilder sb = new StringBuilder("hello"); for (int i = 0; i < 10000; i++) { sb.append(" world"); } String string = sb.toString();

Sullo stesso dispositivo questo accade quasi istantaneamente, in meno di 5 ms. Le visualizzazioni di CPU e memoria sono quasi totalmente piatte, quindi puoi immaginare quanto sia grande questo miglioramento. Si noti tuttavia che per ottenere questa differenza, abbiamo dovuto aggiungere 10mila stringhe, cosa che probabilmente non si fa spesso. Quindi, nel caso in cui aggiungi solo un paio di stringhe una volta, non vedrai alcun miglioramento. A proposito, se lo fai:

 String string = "hello" + " world";

Viene convertito internamente in StringBuilder, quindi funzionerà perfettamente.

Ti starai chiedendo, perché la concatenazione di stringhe nel primo modo è così lenta? È causato dal fatto che le stringhe sono immutabili, quindi una volta create non possono essere modificate. Anche se pensi di modificare il valore di una stringa, in realtà stai creando una nuova stringa con il nuovo valore. In un esempio come:

 String myString = "hello"; myString += " world";

Quello che otterrai in memoria non è 1 stringa "ciao mondo", ma in realtà 2 stringhe. La stringa myString conterrà "hello world", come ti aspetteresti. Tuttavia, la String originale con il valore "hello" è ancora viva, senza alcun riferimento ad essa, in attesa di essere raccolta. Questo è anche il motivo per cui dovresti memorizzare le password in un array di caratteri anziché in una stringa. Se memorizzi una password come una stringa, rimarrà nella memoria in un formato leggibile fino al prossimo GC per un periodo di tempo imprevedibile. Tornando all'immutabilità sopra descritta, la stringa rimarrà in memoria anche se le si assegna un altro valore dopo averlo utilizzato. Se, tuttavia, svuoti l'array di caratteri dopo aver utilizzato la password, questa scomparirà dappertutto.

2. Scegliere il tipo di dati corretto

Prima di iniziare a scrivere codice, dovresti decidere quali tipi di dati utilizzerai per la tua raccolta. Ad esempio, dovresti usare un Vector o un ArrayList ? Bene, dipende dal tuo caso d'uso. Se hai bisogno di una raccolta thread-safe, che consentirà a un solo thread alla volta di lavorare con esso, dovresti scegliere un Vector , poiché è sincronizzato. In altri casi dovresti probabilmente attenerti a un ArrayList , a meno che tu non abbia davvero un motivo specifico per utilizzare i vettori.

Che ne dici del caso in cui desideri una collezione con oggetti unici? Bene, dovresti probabilmente scegliere un Set . Non possono contenere duplicati in base alla progettazione, quindi non dovrai occupartene tu stesso. Esistono diversi tipi di set, quindi scegline uno adatto al tuo caso d'uso. Per un semplice gruppo di elementi unici, puoi utilizzare un HashSet . Se vuoi preservare l'ordine degli elementi in cui sono stati inseriti, scegli un LinkedHashSet . Un TreeSet ordina automaticamente gli elementi, quindi non dovrai chiamare alcun metodo di ordinamento su di esso. Dovrebbe anche ordinare gli elementi in modo efficiente, senza dover pensare ad algoritmi di ordinamento.

I dati dominano. Se hai scelto le giuste strutture dati e organizzato bene le cose, gli algoritmi saranno quasi sempre evidenti. Le strutture dati, non gli algoritmi, sono fondamentali per la programmazione.
— Le 5 regole di programmazione di Rob Pike

L'ordinamento di numeri interi o stringhe è piuttosto semplice. Tuttavia, cosa succede se si desidera ordinare una classe in base a una proprietà? Diciamo che stai scrivendo un elenco di pasti che mangi e memorizzi i loro nomi e timestamp. Come classificheresti i pasti per timestamp dal più basso al più alto? Fortunatamente, è piuttosto semplice. È sufficiente implementare l'interfaccia Comparable nella classe Meal e sovrascrivere la funzione compareTo() . Per ordinare i pasti dal timestamp più basso al più alto, potremmo scrivere qualcosa del genere.

 @Override public int compareTo(Object object) { Meal meal = (Meal) object; if (this.timestamp < meal.getTimestamp()) { return -1; } else if (this.timestamp > meal.getTimestamp()) { return 1; } return 0; }

3. Aggiornamenti sulla posizione

Ci sono molte app là fuori che raccolgono la posizione dell'utente. A tale scopo dovresti utilizzare l'API dei servizi di localizzazione di Google, che contiene molte funzioni utili. C'è un articolo separato sull'uso, quindi non lo ripeterò.

Vorrei solo sottolineare alcuni punti importanti dal punto di vista delle prestazioni.

Prima di tutto, usa solo la posizione più precisa di cui hai bisogno. Ad esempio, se stai facendo alcune previsioni del tempo, non hai bisogno della posizione più precisa. Ottenere solo un'area molto accidentata basata sulla rete è più veloce e più efficiente della batteria. Puoi ottenerlo impostando la priorità su LocationRequest.PRIORITY_LOW_POWER .

Puoi anche usare una funzione di LocationRequest chiamata setSmallestDisplacement() . L'impostazione in metri farà sì che la tua app non riceva notifiche sulla modifica della posizione se era inferiore al valore specificato. Ad esempio, se hai una mappa con ristoranti nelle vicinanze intorno a te e imposti lo spostamento minimo a 20 metri, l'app non richiederà il controllo dei ristoranti se l'utente sta solo camminando in una stanza. Le richieste sarebbero inutili, in quanto non ci sarebbe comunque nessun nuovo ristorante nelle vicinanze.

La seconda regola è richiedere gli aggiornamenti della posizione solo ogni volta che ne hai bisogno. Questo è abbastanza autoesplicativo. Se stai davvero costruendo quell'app per le previsioni del tempo, non è necessario richiedere la posizione ogni pochi secondi, poiché probabilmente non hai previsioni così precise (contattami se lo fai). Puoi utilizzare la funzione setInterval() per impostare l'intervallo richiesto in cui il dispositivo aggiornerà l'app sulla posizione. Se più app continuano a richiedere la posizione dell'utente, ogni app riceverà una notifica a ogni nuovo aggiornamento della posizione, anche se hai un setInterval() più alto. Per evitare che la tua app venga notificata troppo spesso, assicurati di impostare sempre un intervallo di aggiornamento più veloce con setFastestInterval() .

E infine, la terza regola è richiedere gli aggiornamenti della posizione solo se ne hai bisogno. Se visualizzi alcuni oggetti vicini sulla mappa ogni x secondi e l'app va in background, non è necessario conoscere la nuova posizione. Non vi è alcun motivo per aggiornare la mappa se l'utente non può comunque vederla. Assicurati di interrompere l'ascolto degli aggiornamenti sulla posizione quando appropriato, preferibilmente in onPause() . È quindi possibile riprendere gli aggiornamenti in onResume() .

4. Richieste di rete

È molto probabile che la tua app utilizzi Internet per scaricare o caricare dati. In tal caso, hai diversi motivi per prestare attenzione alla gestione delle richieste di rete. Uno di questi sono i dati mobili, che sono molto limitati a molte persone e non dovresti sprecarli.

Il secondo è la batteria. Sia il WiFi che le reti mobili possono consumarne parecchio se vengono utilizzati troppo. Diciamo che vuoi scaricare 1 kb. Per effettuare una richiesta di rete, devi riattivare il cellulare o la radio WiFi, quindi puoi scaricare i tuoi dati. Tuttavia, la radio non si addormenterà subito dopo l'operazione. Rimarrà in uno stato abbastanza attivo per circa 20-40 secondi in più, a seconda del dispositivo e dell'operatore telefonico.

Richieste di rete

Allora, cosa puoi fare al riguardo? Lotto. Per evitare di riattivare la radio ogni due secondi, precarica le cose di cui l'utente potrebbe aver bisogno nei prossimi minuti. Il modo corretto di eseguire il batch è altamente dinamico a seconda della tua app, ma se è possibile, dovresti scaricare i dati di cui l'utente potrebbe aver bisogno nei prossimi 3-4 minuti. Si potrebbero anche modificare i parametri batch in base al tipo di Internet dell'utente o allo stato di carica. Ad esempio, se l'utente è in Wi-Fi durante la ricarica, è possibile precaricare molti più dati rispetto a quando l'utente è su Internet mobile con batteria scarica. Prendere in considerazione tutte queste variabili può essere una cosa difficile, cosa che solo poche persone farebbero. Fortunatamente, però, c'è GCM Network Manager in soccorso!

GCM Network Manager è una classe davvero utile con molti attributi personalizzabili. Puoi programmare facilmente sia attività ripetute che una tantum. Per le attività ripetute è possibile impostare l'intervallo di ripetizione più basso e più alto. Ciò consentirà di raggruppare non solo le tue richieste, ma anche le richieste di altre app. La radio deve essere svegliata solo una volta per un certo periodo e, mentre è attiva, tutte le app in coda scaricano e caricano ciò che dovrebbero. Questo Manager è anche a conoscenza del tipo di rete del dispositivo e dello stato di carica, quindi puoi regolare di conseguenza. Puoi trovare maggiori dettagli ed esempi in questo articolo, ti esorto a dare un'occhiata. Un'attività di esempio è simile a questa:

 Task task = new OneoffTask.Builder() .setService(CustomService.class) .setExecutionWindow(0, 30) .setTag(LogService.TAG_TASK_ONEOFF_LOG) .setUpdateCurrent(false) .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) .setRequiresCharging(false) .build();

A proposito, da Android 3.0, se esegui una richiesta di rete sul thread principale, otterrai un NetworkOnMainThreadException . Questo ti avviserà sicuramente di non farlo di nuovo.

5. Riflessione

La riflessione è la capacità di classi e oggetti di esaminare i propri costruttori, campi, metodi e così via. Di solito viene utilizzato per la compatibilità con le versioni precedenti, per verificare se un determinato metodo è disponibile per una particolare versione del sistema operativo. Se devi usare la riflessione a tale scopo, assicurati di memorizzare nella cache la risposta, poiché l'uso della riflessione è piuttosto lento. Anche alcune librerie ampiamente utilizzate utilizzano Reflection, come Roboguice per l'iniezione di dipendenze. Questo è il motivo per cui dovresti preferire Dagger 2. Per maggiori dettagli sulla riflessione, puoi controllare un post separato.

6. Autobox

L'autoboxing e l'unboxing sono processi di conversione di un tipo primitivo in un tipo di oggetto o viceversa. In pratica significa convertire un int in un intero. Per raggiungere questo obiettivo, il compilatore utilizza internamente la funzione Integer.valueOf() . La conversione non è solo lenta, gli oggetti richiedono anche molta più memoria rispetto ai loro equivalenti primitivi. Diamo un'occhiata a un po' di codice.

 Integer total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

Sebbene ciò richieda in media 500 ms, riscriverlo per evitare l'autoboxing lo accelererà drasticamente.

 int total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

Questa soluzione funziona a circa 2 ms, che è 25 volte più veloce. Se non mi credi, provalo. I numeri saranno ovviamente diversi per dispositivo, ma dovrebbe comunque essere molto più veloce. Ed è anche un passaggio davvero semplice da ottimizzare.

Ok, probabilmente non crei spesso una variabile di tipo Integer in questo modo. Ma che dire dei casi in cui è più difficile evitare? Come in una mappa, dove devi usare Objects, come Map<Integer, Integer> ? Guarda la soluzione che molte persone usano.

 Map<Integer, Integer> myMap = new HashMap<>(); for (int i = 0; i < 100000; i++) { myMap.put(i, random.nextInt()); }

L'inserimento di 100.000 int casuali nella mappa richiede circa 250 ms per l'esecuzione. Ora guarda la soluzione con SparseIntArray.

 SparseIntArray myArray = new SparseIntArray(); for (int i = 0; i < 100000; i++) { myArray.put(i, random.nextInt()); }

Questo richiede molto meno, circa 50 ms. È anche uno dei metodi più semplici per migliorare le prestazioni, poiché non è necessario fare nulla di complicato e anche il codice rimane leggibile. Durante l'esecuzione di un'app chiara con la prima soluzione ha richiesto 13 MB di memoria, l'utilizzo di int primitivi ha richiesto qualcosa sotto i 7 MB, quindi solo la metà.

SparseIntArray è solo una delle fantastiche raccolte che possono aiutarti a evitare l'autoboxing. Una mappa come Map<Integer, Long> potrebbe essere sostituita da SparseLongArray , poiché il valore della mappa è di tipo Long . Se guardi il codice sorgente di SparseLongArray , vedrai qualcosa di piuttosto interessante. Sotto il cofano, ci sono fondamentalmente solo un paio di array. Puoi anche usare un SparseBooleanArray in modo simile.

Se leggi il codice sorgente, potresti aver notato una nota che dice che SparseIntArray può essere più lento di HashMap . Ho sperimentato molto, ma per me SparseIntArray è sempre stato migliore sia in termini di memoria che di prestazioni. Immagino che dipenda ancora da te quale scegliere, sperimentare con i tuoi casi d'uso e vedere quale si adatta di più a te. Sicuramente avrai gli SparseArrays in testa quando usi le mappe.

7. OnDraw

Come ho detto sopra, quando ottimizzi le prestazioni, probabilmente vedrai il massimo vantaggio nell'ottimizzazione del codice che viene eseguito spesso. Una delle funzioni che funzionano molto è onDraw() . Potrebbe non sorprenderti che sia responsabile del disegno delle viste sullo schermo. Poiché i dispositivi di solito funzionano a 60 fps, la funzione viene eseguita 60 volte al secondo. Ogni frame ha 16 ms per essere completamente gestito, inclusa la sua preparazione e disegno, quindi dovresti davvero evitare le funzioni lente. Solo il thread principale può disegnare sullo schermo, quindi dovresti evitare di eseguire operazioni costose su di esso. Se blocchi il thread principale per diversi secondi, potresti visualizzare la famigerata finestra di dialogo Application Not Responding (ANR). Per ridimensionare le immagini, il lavoro del database, ecc., utilizzare un thread in background.

Se pensi che i tuoi utenti non noteranno quel calo del frame rate, ti sbagli!
Twitta

Ho visto alcune persone provare ad abbreviare il loro codice, pensando che sarà più efficiente in questo modo. Questa non è sicuramente la strada da percorrere, poiché un codice più breve non significa assolutamente un codice più veloce. In nessun caso misurare la qualità del codice in base al numero di righe.

Una delle cose che dovresti evitare in onDraw() è l'allocazione di oggetti come Paint. Prepara tutto nel costruttore, in modo che sia pronto per il disegno. Anche se hai ottimizzato onDraw() , dovresti chiamarlo solo tutte le volte che devi. Cosa c'è di meglio che chiamare una funzione ottimizzata? Bene, non chiamando nessuna funzione. Nel caso in cui tu voglia disegnare del testo, c'è una funzione di aiuto piuttosto ordinata chiamata drawText() , dove puoi specificare cose come il testo, le coordinate e il colore del testo.

8. Viewholder

Probabilmente conosci questo, ma non posso saltarlo. Il modello di progettazione Viewholder è un modo per semplificare lo scorrimento degli elenchi. È una sorta di memorizzazione nella cache delle viste, che può ridurre seriamente le chiamate a findViewById() e aumentare le visualizzazioni memorizzandole. Può assomigliare a questo.

 static class ViewHolder { TextView title; TextView text; public ViewHolder(View view) { title = (TextView) view.findViewById(R.id.title); text = (TextView) view.findViewById(R.id.text); } }

Quindi, all'interno della funzione getView() del tuo adattatore, puoi verificare se hai una vista utilizzabile. In caso contrario, ne crei uno.

 ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, viewGroup, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.title.setText("Hello World");

Puoi trovare molte informazioni utili su questo modello su Internet. Può essere utilizzato anche nei casi in cui la visualizzazione elenco contiene più tipi diversi di elementi, come alcune intestazioni di sezione.

9. Ridimensionamento delle immagini

È probabile che la tua app conterrà alcune immagini. Nel caso in cui stai scaricando alcuni JPG dal web, possono avere risoluzioni davvero enormi. Tuttavia, i dispositivi su cui verranno visualizzati saranno molto più piccoli. Anche se scatti una foto con la fotocamera del tuo dispositivo, è necessario ridimensionarla prima di visualizzarla poiché la risoluzione della foto è molto maggiore della risoluzione del display. Ridimensionare le immagini prima di visualizzarle è una cosa fondamentale. Se provassi a visualizzarli a piena risoluzione, esauriresti la memoria abbastanza rapidamente. C'è molto scritto sulla visualizzazione efficiente delle bitmap nei documenti Android, proverò a riassumerlo.

Quindi hai una bitmap, ma non ne sai nulla. C'è un utile flag di bitmap chiamato inJustDecodeBounds al tuo servizio, che ti consente di scoprire la risoluzione della bitmap. Supponiamo che la tua bitmap sia 1024x768 e che ImageView utilizzato per visualizzarlo sia solo 400x300. Dovresti continuare a dividere la risoluzione della bitmap per 2 finché non è ancora più grande dell'ImageView specificato. Se lo fai, eseguirà il downsampling della bitmap di un fattore 2, ottenendo una bitmap di 512x384. La bitmap sottocampionata utilizza 4 volte meno memoria, il che ti aiuterà molto a evitare il famoso errore OutOfMemory.

Ora che sai come farlo, non dovresti farlo. ... Almeno, non se la tua app fa molto affidamento sulle immagini e non sono solo 1-2 immagini. Sicuramente evita cose come il ridimensionamento e il riciclaggio delle immagini manualmente, usa alcune librerie di terze parti per questo. I più popolari sono Picasso di Square, Universal Image Loader, Fresco di Facebook o il mio preferito, Glide. C'è un'enorme comunità attiva di sviluppatori intorno ad esso, quindi puoi trovare molte persone utili anche nella sezione dei problemi su GitHub.

10. Modalità rigorosa

Strict Mode è uno strumento di sviluppo abbastanza utile che molte persone non conoscono. Di solito viene utilizzato per rilevare le richieste di rete o gli accessi al disco dal thread principale. Puoi impostare quali problemi dovrebbe cercare la Modalità rigorosa e quale penalità dovrebbe attivare. Un esempio di Google si presenta così:

 public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }

Se vuoi rilevare tutti i problemi che la Modalità rigorosa può trovare, puoi anche usare detectAll() . Come con molti suggerimenti sulle prestazioni, non dovresti provare ciecamente a correggere tutti i rapporti sulla modalità rigorosa. Basta indagare e, se sei sicuro che non sia un problema, lascialo stare. Assicurati inoltre di utilizzare la modalità rigorosa solo per il debug e di averla sempre disabilitata nelle build di produzione.

Prestazioni di debug: il modo professionale

Vediamo ora alcuni strumenti che possono aiutarti a trovare colli di bottiglia, o almeno mostrare che qualcosa non va.

1. Monitor Android

Questo è uno strumento integrato in Android Studio. Per impostazione predefinita, puoi trovare il monitor Android nell'angolo in basso a sinistra e puoi passare tra 2 schede lì. Logcat e monitor. La sezione Monitor contiene 4 diversi grafici. Rete, CPU, GPU e memoria. Sono abbastanza autoesplicativi, quindi li esaminerò rapidamente. Ecco uno screenshot dei grafici presi durante l'analisi di alcuni JSON mentre vengono scaricati.

Monitor Android

La parte Rete mostra il traffico in entrata e in uscita in KB/s. La parte CPU mostra l'utilizzo della CPU in percentuale. Il monitor GPU mostra quanto tempo è necessario per eseguire il rendering dei frame di una finestra dell'interfaccia utente. Questo è il monitor più dettagliato di questi 4, quindi se vuoi maggiori dettagli a riguardo, leggi questo.

Infine abbiamo il Memory monitor, che probabilmente utilizzerai di più. Per impostazione predefinita mostra la quantità corrente di memoria libera e allocata. Puoi anche forzare una Garbage Collection con esso, per verificare se la quantità di memoria utilizzata diminuisce. Ha una funzione utile chiamata Dump Java Heap, che creerà un file HPROF che può essere aperto con il visualizzatore e l'analizzatore HPROF. Ciò ti consentirà di vedere quanti oggetti hai allocato, quanta memoria viene occupata da cosa e forse quali oggetti stanno causando perdite di memoria. Imparare a usare questo analizzatore non è il compito più semplice là fuori, ma ne vale la pena. La prossima cosa che puoi fare con il monitor della memoria è eseguire un monitoraggio dell'allocazione a tempo, che puoi avviare e interrompere come desideri. Potrebbe essere utile in molti casi, ad esempio durante lo scorrimento o la rotazione del dispositivo.

2. Sovraccarico della GPU

Questo è un semplice strumento di supporto, che puoi attivare nelle Opzioni sviluppatore dopo aver abilitato la modalità sviluppatore. Seleziona Debug GPU overdraw, "Mostra aree di overdraw" e lo schermo assumerà dei colori strani. Va bene, è quello che dovrebbe succedere. I colori indicano quante volte una particolare area è stata superata. Il vero colore significa che non c'è stato uno scoperto, questo è ciò a cui dovresti mirare. Blu significa uno scoperto, verde significa due, rosa tre, rosso quattro.

Sovraccarico della GPU

Anche se vedere il vero colore è il migliore, vedrai sempre alcuni scoperti, specialmente su testi, cassetti di navigazione, finestre di dialogo e altro. Quindi non cercare di sbarazzartene completamente. Se la tua app è bluastra o verdastra, probabilmente va bene. Tuttavia, se vedi troppo rosso su alcune semplici schermate, dovresti indagare su cosa sta succedendo. Potrebbero essere troppi frammenti impilati l'uno sull'altro, se continui ad aggiungerli invece di sostituirli. Come accennato in precedenza, il disegno è la parte più lenta delle app, quindi non ha senso disegnare qualcosa se ci saranno più di 3 livelli disegnati su di esso. Sentiti libero di controllare le tue app preferite con esso. Vedrai che anche le app con oltre un miliardo di download hanno aree rosse, quindi rilassati quando cerchi di ottimizzare.

3. Rendering della GPU

Questo è un altro strumento delle opzioni sviluppatore, chiamato Rendering della GPU del profilo. Dopo averlo selezionato, seleziona "Sullo schermo come barre". Noterai alcune barre colorate che appaiono sullo schermo. Poiché ogni applicazione ha barre separate, stranamente la barra di stato ha le sue e, nel caso in cui tu abbia pulsanti di navigazione del software, anche loro hanno le proprie barre. Ad ogni modo, le barre vengono aggiornate man mano che interagisci con lo schermo.

Rendering della GPU

Le barre sono composte da 3-4 colori e, secondo i documenti Android, le loro dimensioni contano davvero. Più piccolo è, meglio è. In basso hai il blu, che rappresenta il tempo utilizzato per creare e aggiornare gli elenchi di visualizzazione della vista. Se questa parte è troppo alta, significa che c'è molto disegno della vista personalizzato o molto lavoro svolto nelle funzioni onDraw() . Se hai Android 4.0+, vedrai una barra viola sopra quella blu. Questo rappresenta il tempo impiegato per trasferire le risorse al thread di rendering. Poi arriva la parte rossa, che rappresenta il tempo trascorso dal renderer 2D di Android inviando comandi a OpenGL per disegnare e ridisegnare elenchi di visualizzazione. In alto c'è la barra arancione, che rappresenta il tempo che la CPU attende che la GPU finisca il suo lavoro. Se è troppo alto, l'app sta facendo troppo lavoro sulla GPU.

Se sei abbastanza bravo, c'è un altro colore sopra l'arancione. È una linea verde che rappresenta la soglia di 16 ms. Poiché il tuo obiettivo dovrebbe essere l'esecuzione della tua app a 60 fps, hai 16 ms per disegnare ogni fotogramma. Se non lo fai, alcuni frame potrebbero essere saltati, l'app potrebbe diventare a scatti e l'utente se ne accorgerebbe sicuramente. Presta particolare attenzione alle animazioni e allo scorrimento, è qui che la fluidità conta di più. Anche se puoi rilevare alcuni fotogrammi saltati con questo strumento, non ti aiuterà davvero a capire dove si trova esattamente il problema.

4. Visualizzatore gerarchia

Questo è uno dei miei strumenti preferiti là fuori, in quanto è davvero potente. Puoi avviarlo da Android Studio tramite Strumenti -> Android -> Monitor dispositivo Android, oppure è anche nella cartella sdk/tools come "monitor". Puoi anche trovare un eseguibile hierarachyviewer autonomo lì, ma poiché è deprecato dovresti aprire il monitor. Comunque apri Android Device Monitor, passa alla prospettiva del Visualizzatore gerarchia. Se non vedi alcuna app in esecuzione assegnata al tuo dispositivo, ci sono un paio di cose che puoi fare per risolverlo. Prova anche a dare un'occhiata a questo thread di problemi, ci sono persone con tutti i tipi di problemi e tutti i tipi di soluzioni. Qualcosa dovrebbe funzionare anche per te.

Con Hierarchy Viewer, puoi ottenere una panoramica davvero accurata delle tue gerarchie di viste (ovviamente). Se vedi ogni layout in un XML separato, potresti facilmente individuare visualizzazioni inutili. Tuttavia, se continui a combinare i layout, può facilmente creare confusione. Uno strumento come questo semplifica l'individuazione, ad esempio, di alcuni RelativeLayout, che ha solo 1 figlio, un altro RelativeLayout. Ciò rende uno di loro rimovibile.

Evita di chiamare requestLayout() , poiché provoca l'attraversamento dell'intera gerarchia di viste, per scoprire quanto dovrebbe essere grande ciascuna vista. Se c'è qualche conflitto con le misurazioni, la gerarchia potrebbe essere attraversata più volte, cosa che se accade durante alcune animazioni, farà sicuramente saltare alcuni fotogrammi. Se vuoi saperne di più su come Android disegna le sue viste, puoi leggere questo. Diamo un'occhiata a una vista vista in Hierarchy Viewer.

Visualizzatore gerarchia

L'angolo in alto a destra contiene un pulsante per massimizzare l'anteprima di una vista particolare in una finestra autonoma. Sotto di esso puoi anche vedere l'anteprima effettiva della vista nell'app. L'elemento successivo è un numero, che rappresenta quanti figli ha la vista data, inclusa la vista stessa. Se selezioni un nodo (preferibilmente quello principale) e premi "Ottieni tempi di layout" (3 cerchi colorati), avrai altri 3 valori riempiti, insieme a cerchi colorati che appaiono etichettati misura, layout e disegno. Potrebbe non essere scioccante che la fase di misurazione rappresenti il ​​tempo impiegato per misurare la vista data. La fase di layout riguarda il tempo di rendering, mentre il disegno è l'operazione di disegno vera e propria. Questi valori e colori sono relativi tra loro. Quello verde significa che la vista viene visualizzata nel 50% superiore di tutte le viste nell'albero. Il giallo indica il rendering nel 50% più lento di tutte le viste nell'albero, il rosso indica che la vista data è una delle più lente. Poiché questi valori sono relativi, ci saranno sempre quelli rossi. Semplicemente non puoi evitarli.

Sotto i valori hai il nome della classe, come "TextView", un ID vista interno dell'oggetto e android:id della vista, che hai impostato nei file XML. Ti esorto a prendere l'abitudine di aggiungere ID a tutte le viste, anche se non le fai riferimento nel codice. Semplificherà l'identificazione delle viste in Hierarchy Viewer e, nel caso in cui tu abbia test automatizzati nel tuo progetto, renderà anche il targeting degli elementi molto più veloce. Ciò consentirà a te e ai tuoi colleghi di scriverle di risparmiare tempo. L'aggiunta di ID agli elementi aggiunti nei file XML è piuttosto semplice. Ma che dire degli elementi aggiunti dinamicamente? Bene, risulta essere anche molto semplice. Basta creare un file ids.xml all'interno della cartella dei valori e digitare i campi richiesti. Può assomigliare a questo:

 <resources> <item name="item_title" type="id"/> <item name="item_body" type="id"/> </resources>

Quindi nel codice puoi usare setId(R.id.item_title) . Non potrebbe essere più semplice.

Ci sono un altro paio di cose a cui prestare attenzione quando si ottimizza l'interfaccia utente. In genere dovresti evitare le gerarchie profonde mentre preferisci quelle poco profonde, magari larghe. Non utilizzare layout non necessari. Ad esempio, puoi probabilmente sostituire un gruppo di LinearLayouts nidificati con un RelativeLayout o un TableLayout . Sentiti libero di sperimentare layout diversi, non usare sempre LinearLayout e RelativeLayout . Prova anche a creare alcune viste personalizzate quando necessario, può migliorare significativamente le prestazioni se fatto bene. Ad esempio, sapevi che Instagram non utilizza TextViews per visualizzare i commenti?

Puoi trovare qualche informazione in più su Hierarchy Viewer sul sito degli sviluppatori Android con descrizioni di diversi riquadri, usando lo strumento Pixel Perfect, ecc. Un'altra cosa che vorrei sottolineare è l'acquisizione delle viste in un file .psd, che può essere fatto da il pulsante "Cattura i livelli della finestra". Ogni vista sarà in un livello separato, quindi è davvero semplice nasconderla o cambiarla in Photoshop o GIMP. Oh, questo è un altro motivo per aggiungere un ID a ogni visualizzazione possibile. Farà in modo che i livelli abbiano nomi che hanno davvero un senso.

Troverai molti più strumenti di debug nelle Opzioni sviluppatore, quindi ti consiglio di attivarli e vedere cosa stanno facendo. Che cosa potrebbe andare storto?

Il sito degli sviluppatori Android contiene una serie di best practice per le prestazioni. Coprono molte aree diverse, inclusa la gestione della memoria, di cui non ho davvero parlato. L'ho ignorato silenziosamente, perché gestire la memoria e tenere traccia delle perdite di memoria è una storia completamente separata. L'uso di una libreria di terze parti per visualizzare in modo efficiente le immagini aiuterà molto, ma se hai ancora problemi di memoria, dai un'occhiata a Leak Canary di Square o leggi questo.

Avvolgendo

Quindi, questa era la buona notizia. La cattiva notizia è che l'ottimizzazione delle app Android è molto più complicata. Ci sono molti modi per fare tutto, quindi dovresti avere familiarità con i pro e i contro di essi. Di solito non esiste una soluzione proiettile d'argento che abbia solo vantaggi. Solo comprendendo cosa sta succedendo dietro le quinte sarai in grado di scegliere la soluzione migliore per te. Solo perché il tuo sviluppatore preferito dice che qualcosa è buono, non significa necessariamente che sia la soluzione migliore per te. Ci sono molte più aree di cui discutere e più strumenti di profilazione che sono più avanzati, quindi potremmo trovarli la prossima volta.

Assicurati di imparare dai migliori sviluppatori e dalle migliori aziende. Puoi trovare un paio di centinaia di blog di ingegneria a questo link. Ovviamente non sono solo cose relative ad Android, quindi se sei interessato solo ad Android, devi filtrare il blog in particolare. Consiglio vivamente i blog di Facebook e Instagram. Anche se l'interfaccia utente di Instagram su Android è discutibile, il loro blog di ingegneria ha alcuni articoli davvero interessanti. Per me è fantastico che sia così facile vedere come vengono fatte le cose in aziende che gestiscono centinaia di milioni di utenti ogni giorno, quindi non leggere i loro blog sembra pazzesco. Il mondo sta cambiando molto velocemente, quindi se non cerchi costantemente di migliorare, imparare dagli altri e utilizzare nuovi strumenti, rimarrai indietro. Come diceva Mark Twain, una persona che non legge non ha alcun vantaggio rispetto a una che non sa leggere.