Alessandro Del Sole's Blog

{ A programming space about Microsoft® .NET® }
posts - 1902, comments - 2047, trackbacks - 352

My Links

News

Your host

This is me! Questo spazio è dedicato a Microsoft® .NET®, di cui sono molto appassionato :-)

Cookie e Privacy

Disabilita cookie ShinyStat

Microsoft MVP

My MVP Profile

Microsoft Certified Professional

Microsoft Specialist

Il mio libro su VB 2015!

Pre-ordina il mio libro su VB 2015 Pre-ordina il mio libro "Visual Basic 2015 Unleashed". Clicca sulla copertina per informazioni!

Il mio libro su WPF 4.5.1!

Clicca sulla copertina per informazioni! E' uscito il mio libro "Programmare con WPF 4.5.1". Clicca sulla copertina per informazioni!

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.
If you're not an Italian user, please visit my English blog

Le vostre visite

I'm a VB!

Guarda la mia intervista a Seattle

Follow me on Twitter!

Altri spazi

GitHub
I miei progetti open-source su GitHub

Article Categories

Archives

Post Categories

Image Galleries

Privacy Policy

Xamarin.Forms: considerazioni sulle performance e come migliorarle

Per lavoro mi sto occupando di un'app scritta con Xamarin.Forms in cui le performance non sono un optional e quindi ho voluto raccogliere una serie di considerazioni e analisi fatte, con alcune piccole premesse. Di seguito alle premesse, farò le considerazioni del caso. Non inserisco codice né esempi, perché voglio che leggiate e non vi distraiate . Troverete tutto approfondito nella documentazione, linkata successivamente.

Premessa 1: E' davvero sempre colpa di Xamarin.Forms se ho problemi di performance?

Risposta breve: no. Ricordiamoci che Xamarin genera app native, ovviamente si porta dietro le proprie librerie e le sue modalità di compilazione e questo può causare degli scarti in eccesso rispetto a tool, linguaggi e framework nativi. Sono anche vere due cose: il sistema operativo fa differenza (vedi Premessa 2) e molta importanza è rivestita da come noi scriviamo codice e da come noi progettiamo la UI. Prendiamoci le nostre responsabilità, insomma, e non diamo tutta la colpa a Xamarin.

Premessa 2: Sistemi operativi diversi, comportamenti diversi

Non sto qui a giudicare quale OS sia il migliore. Ognuno avrà i propri benchmark e le proprie idee. Certo è che iOS, Android e Windows 10 Mobile sono diversi tra loro e hanno modalità diverse di gestire sistema e hardware. La stessa operazione su tre sistemi diversi avrà performance e tempi di esecuzione diversi. Teniamolo in debita considerazione e proviamo, ove possibile, a replicare la stessa operazione su tutti e tre.

Premessa 3: Lo sviluppatore (ancora) conta

Lo sviluppatore è un creativo e quindi può trovare modi diversi di fare la stessa operazione. A volte modi diversi hanno impatti diversi. Questo vale anche per la user interface, con tanti piccoli accorgimenti. Altre volte si scrive codice ridondante, altre volte semplicemente non consideriamo il fatto che esiste documentazione al riguardo. Per esempio, questa pagina dedicata all'ottimizzazione delle performance di Xamarin.Forms.

Premessa 4: Questa non è la Bibbia!

Questo post contiene una serie di consigli e di considerazioni, ciò non vuol dire che a) sia la Bibbia (per quello c'è la documentazione) e che b) tutto quanto descritto debba sempre trovare applicazione. E' chiaro che posso avere esigenza di soddisfare un requisito del cliente e che, per farlo, debba accettare dei compromessi.

Oggetti, memoria e Garbage Collection

Se la nostra app istanzia tantissimi oggetti, c'è un impatto sulla memoria. Bisogna capire se è un comportamento atteso e in modo fluido oppure no, nel qual caso possiamo sfruttare un nuovo tool chiamato Xamarin Profiler che, tra le varie cose, analizza l'utilizzo della memoria e degli oggetti istanziati. Nel caso di Android, il debugger di Mono ci dice anche quando ci sono Garbage Collection scatenate dal sistema. Frequenti GC sono sintomo di problemi. GC scatenate dal codice tramite System.GC.Collect() causano ovvi performance overhead, quindi sono da evitare il più possibile (o al massimo ricorrere alle c.d. minor GC). In sintesi, l'intervento del Garbage Collector ha un peso: se è frequente, ha un peso maggiore e se lo invoco da codice, magari ripetutamente, ha un peso.

Multi-threading

I device moderni non hanno alcun problema col multi-threading, anzi. E' buona pratica demandare ad un thread secondario (ad esempio con Task.Run) l'esecuzione di operazioni intensive per la CPU, in modo che il thread della UI rimanga libero. Al riguardo, prima considerazione: async/await è preferibile, vedi paragrafo successivo. Seconda considerazione: l'operazione che demando al thread secondario, è davvero intensiva per la CPU? Terza e più importante considerazione: mentre eseguire un thread secondario non ha un particolare impatto, lo avrà invece se la cosa è ciclica. Mi spiego meglio: se ad esempio genero tanti elementi a runtime tramite un loop e per ognuno faccio fare delle cose in un thread secondario che poi viene chiuso per tornare al thread principale, ho un certo impatto negativo, poiché il sistema deve switchare di continuo tra thread. La domanda a cui devo rispondere è quindi: sto davvero eseguendo operazioni CPU-intensive o posso fare in altro modo?

Il pattern Async/Await

Credo che ormai lo conosciamo tutti. Il suo utilizzo nelle app è cruciale. Mi permette di eseguire operazioni potenzialmente long-running in modalità asincrona, senza bloccare la UI e senza dover ricorrere a thread secondari. Quindi, riagganciandomi ai thread: l'operazione che demando al thread secondario è realmente CPU-intensive o solamente long-running?
Altra questione: metodi sincroni che eseguono operazioni long-running all'interno del thread della UI, la bloccano. Se dichiaro un metodo asincrono ma al suo interno non c'è nessun await o nessun'altra operazione asincrona invocata, verrà considerato sincrono e quindi non ho risolto nulla . Attenzione quindi agli operatori.

L'interfaccia utente: i giusti Layout

La progettazione dell'interfaccia ha un impatto importantissimo sulle performance. Spesso dimentichiamo che ogni controllo o panel che scriviamo nello XAML corrisponde non solo all'istanza di una classe in memoria, ma anche ad un elemento visivo che deve essere "renderizzato" sul display e che quindi ha un costo per la CPU. Per esempio, la documentazione ci riferisce che il RelativeLayout, rispetto ad altri, causa problemi di performance e quindi è meglio non usarlo. Ovviamente se esiste c'è un perché, quindi l'utilizzo dovrebbe essere limitato a tutte quelle situazioni in cui lo stesso risultato non si possa ottenere con altri panel. Se però in una pagina ho tanti RelativeLayout e magari ho un data template che può contenerne n, posso avere un problema di performance nel disegno. Stesso discorso vale, ad esempio, per panel nidificati tra loro quando non serve. Se per esempio devo affiancare in verticale una serie di controlli, posso farlo attraverso un unico StackLayout. Non è detto che mi servano vari StackLayout all'interno di quello principale solo per raggruppare controlli. Ogni panel, infatti, dev'essere istanziato e renderizzato: ha un costo. Se non posso farne a meno, ok. Se però posso farne a meno, è bene che faccia attenzione.

L'interfaccia utente: popolamento a runtime

Spesso abbiamo necessità di generare elementi dell'interfaccia a runtime, magari tramite un foreach. Tutti questi elementi vanno a finire poi in un panel (Grid, StackLayout, ecc.) che li conterrà. Se questo contenitore viene dichiarato nello XAML e popolato a runtime, il sistema continuerà a modificarne lo stato ad ogni nuovo elemento aggiunto e quindi ci sarà un peggioramento delle performance (ovviamente se gli elementi sono tanti). Se questo contenitore viene invece generato da codice, popolato e solo al termine del popolamento assegnato all'interfaccia, il sistema lo disegnerà una sola volta. Chiaro, dovrà disegnare anche tutti gli elementi che contiene, ma il suo stato non viene modificato di continuo.

L'interfaccia utente: le immagini

Se un'app fa uso di immagini, la cosa migliore è che ne preveda diverse risoluzioni a seconda del tipo di device su cui girerà. Ovviamente, almeno per quanto riguarda le immagini statiche tipo loghi e simili, eviteremo immagini la cui dimensione è di vari megabytes. Sempre a livello di immagini statiche, Xamarin lavora meglio se le dichiariamo direttamente nello XAML.

Tap gesture ed eventi

E' abbastanza normale aggiungere delle cosiddette tap gesture a controlli che, by design, non supportino eventi tipo Clicked, Tapped o simili. E' il tipico caso delle Label. Se ho una pagina che, alla sua inizializzazione, aggiunge delle tap gesture e che viene visualizzata varie volte nel ciclo di vita dell'app, è bene che nel suo OnDisappearing ne faccia anche il remove. Stesso dicorso vale per i gestori di evento. Se mi metto in ascolto di un evento (operatore += o lambda) è bene che rimuova l'ascolto quando non serve più (operatore -=).

Performance percepite: OnAppearing

Non sempre, ma in alcuni casi può essere utile fare l'override del metodo OnAppearing che ha ogni pagina per fare delle elaborazioni. Questo metodo viene chiamato appena prima che la pagina sia visibile, dopo l'invocazione del costruttore, mentre quest'ultimo viene chiamato quando la pagina dev'essere inizializzata.

Compilazione XAML

Esiste la possibilità di compilare lo XAML direttamente in Intermediate Language, in modo da ridurre le dimensioni dell'assembly finale e da rimuovere parte del tempo necessario al caricamento e all'istanza di elementi. Inoltre, questa opzione verifica la validità dello XAML a compile time, al contrario di quanto avviene per default (ossia a runtime). Non è abilitata di default, bisogna farlo da codice a livello di classe o di assembly.

Elenchi, virtualizzazione e caching

Parliamo di elenchi di dati e di ListView, tipicamente in data-binding a una collection e quindi con un data template. Se quest'ultimo è composto da un numero essenziale di elementi e senza troppi orpelli, è molto meglio. Inoltre la ListView consente di abilitare un'opzione di caching degli elenchi, in modo da velocizzarne il ri-caricamento.

Uso di controlli nativi

Dalla versione 2.2, Xamarin.Forms consente l'utilizzo di controlli nativi invece dei suoi. Sicuramente più performante.

Conclusioni

Come anticipato, queste sono considerazioni fatte sulla pratica e soprattutto con i giusti spunti della documentazione ufficiale. Tutto va sempre poi collocato nel giusto contesto. Se avete altri consigli o se avete obbiezioni, lasciate pure un commento.

Alessandro

Print | posted on lunedì 19 dicembre 2016 00:00 | Filed Under [ Xamarin ]

Powered by:
Powered By Subtext Powered By ASP.NET