Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Xamarin.Forms : Améliorer les performances

Si nous avons vu que les Xamarin.Forms produisent bien des applications natives ce simple terme n’est pas forcément synonyme de performances nous le savons tous. Entre une librairies et une bonne application il y a tout le savoir-faire du développeur !

La nativité

Bien entendu et même si nous approchons de Noël, je ne parle pas de la naissance d’un mystique juif en Palestine il y a un peu plus de 2000 ans mais bien de la nature de Xamarin qui produit des applications natives.

Je ne m’étendrai pas puisque j’ai traité de ce sujet en détail dans l’un de mes derniers articles.

Donc c’est du natif. Mais le fait d’être “natif” est aussi utilisé dans une sorte de perversion de son sens original, une façon de dire “c’est forcément ‘mieux’” entendre par là que cela serait forcément plus rapide, plus performant.

Or il n’en est rien. Il existe des procédés qui enveloppent des tas de choses interprétées au runtime dans une coquille compilée en natif et les performances sont loin d’être au rendez-vous, c’est une ruse, une simple ruse commerciale pour dire qu’on fait du natif alors qu’on n’en fait pas. Des tas de solutions cross-plateformes ou ont utilisé cette feinte grossière.

Allons même plus loin XAML est traité par défaut par WPF, Silverlight, WinRT et UWP comme une ressource qui sera relue et hydratée au runtime. les Xamarin.Forms n’agissent pas autrement. On peut considérer que cela n’est pas forcément très performant.

Charger une ressource XAML qui n’est qu’une forme de sérialisation XML, l’interpréter et recréer les objets dans l’ordre avec toutes leurs propriétés et leurs relations cela prend du temps, bien plus qu’un code IL (traduit en binaire avant son exécution rappelons-le, .NET n’est pas interprété).

Donc natif ne veut pas dire rapide ou lent. Juste natif…

Xamarin.Forms et les performances

Comme j’ai déjà pu l’expliquer Xamarin.Forms n’est pas réellement une surcouche au sens des librairies cross-plateformes qu’on peut trouver en Java ou en JS par exemple. Dans ces derniers cas les contrôles sont réellement des objets nouveaux développés spécifiquement, ils ont le même aspect sur toutes les plateformes.

Xamarin.Forms utilise un autre procédé. Les contrôles sont en réalité dual. Il y a d’une part le contrôle vu par l’application, lui est toujours identique sur toutes les plateformes, et d’autre part le contrôle réel qui est utilisé au runtime qui lui est toujours un contrôle natif de la plateforme.

La glue entre ces deux facettes d’un même contrôle est réalisée par un Renderer. Un objet de rendu visuel. C’est lui qui traduit le contrôle Xamarin.Forms en contrôle réel de la plateforme.

Au runtime c’est donc bien une application compilée native qui tourne et au niveau de son UI ce sont bien uniquement des contrôles natifs qui sont instanciés. Il n’y a donc aucune différence entre une application XForms et une application développée avec un environnement purement natif.

Toutefois il faut admettre que la présence d’un “proxy” entre le contrôle réel et l’application ajoute forcément une légère surconsommation de mémoire et peut-être quelques cycles processeurs ici ou là. Toutefois cela n’a d’impact réel qu’au moment de la création des objets, ensuite une Liste est une liste native, ces lignes affichent des contrôles natifs. Les interactions utilisateur / UI se font nativement aussi. C’est uniquement le feedback éventuel vers l’application qui peut être un poil plus long en raison de la présence d’un proxy. Mais ces interactions sont déjà ralenties et bien plus par la simple existence du Binding par exemple ou le choix d’architectures fortement découplées comme MVVM. Au lieu d’un lien direct entre UI et code-behind on crée en effet tout un système complexe d’indirections, d’interfaces, de services, de bindings, de conteneur IoC, etc, etc… S’imaginer que cela est “gratuit” en termes de performances serait naïf.

S’il fallait s’interroger sur les performances pures il faudrait bien plus s’inquiéter de ces pratiques pourtant jugées comme étant de “bonnes pratiques” plutôt que sur les petits proxy des contrôles Xamarin.Forms.

Rappelons que je parle bien des Xamarin.Forms et non de Xamarin.Android ou Xamarin.iOS qui eux sont natifs de bout en bout, l’UI se codant avec les procédés natifs. Les XForms viennent justement au dessus ces systèmes natifs pour rendre le code cross-plateforme.

La stratégie adoptée par les XForms est d’une grande intelligence car elle permet :

  • D’utiliser une abstraction des contrôles identique sur toutes les plateformes
  • Tout en faisant s’exécuter uniquement des contrôles natifs
  • Mais grâce aux Renderers et malgré le côté cross-plateforme le développeur peut agir à sa guise sur eux, voire en fournir d’autres pour personnaliser toute UI avec du code natif sans avoir à décrocher des XForms.

Côté performances il faut bien admettre que la présence des Renderers et des proxy de contrôles peuvent faire perdre un peu de rapidité. Mais utiliser XAML en est une aussi, programmer en MVVM l’est encore plus et même utiliser .NET et son aspect “managé” l’est aussi.

Loin de moi l’idée de refaire le match sur les performances des environnements managés, tout a été dit depuis des années. L’apport, le confort, la sécurité que cela offre font que .NET est toujours prisé et que les progrès du hardware ont largement gommé les petites pertes en performances. Certes on peut encore choisir du C pour du code ultra sensible qui doit aller le plus vite possible, mais cela limite les cas à des traitements que le développeur LOB standard ne programme pas tous les jours (voire jamais dans sa carrière : traitement complexe d’images ou de son par exemple).

De plus aller dans ce sens d’une “pureté originelle” est très dangereux. La pureté est souvent un fantasme plus qu’une réalité d’ailleurs… Rien de vivant n’est pur car sinon il s’éteint sans lignée. Ce sont les variations génétiques, la diversité biologique qui font que la vie peut perdurer en s’adaptant. C’est la même chose pour les programmes informatiques. Leur écriture s’appuie sur des évolutions successives qui ont été retenues parce qu’elles sont efficaces, les autres ont été éliminées. Il existe bien un darwinisme informatique.

Donc si MVVM ralentit les choses, si XAML par nature crée une distance entre instances et représentations de celles-ci, si toute forme de sérialisation est une perte de temps, si le managé fait perdre des performances, etc, si on vise une “pureté absolue” et des performances brutes, dans ce cas là on est obligé de se dire qu’il faut revenir à l’assembleur (et encore) et n’utiliser que des écrans vert avec matrice de caractères hardcodées (comme c’était le cas sur les premiers PC). Rien ne peut aller plus vite.

J’ai même connu en exploitation les derniers ordinateurs sans aucun écran (sans les programmer je ne suis pas si vieux  !) … Une table avec une imprimante large, un clavier, deux rubans de couleur. Les ordres tapés comme sur une machine à écrire était imprimés immédiatement dans une couleur donnée, les réponses de l’ordinateur était imprimées à la suite dans l’autre couleur. Aucune perte de temps à dessiner des fenêtres en 32 bits (24 plus les 8 du canal alpha)… Pas besoin de carte graphique ni rien de tout cela. Un “vrai” ordinateur, “pur”, ou rien de futile n’existe, juste la puissance. Pas de fioritures.

C’est bien joli mais personne ne souhaiterait encore travailler sur ce type d’ordinateur très “pur”, et certainement pas nos utilisateurs !

Bref la course à la pureté à une limite. Celle d’être déjà en natif et celle du confort, de la sécurité minimum qu’on s’impose par nécessité ou simplement par mode ou pour séduire l’utilisateur, tout autant que les normes que nous nous imposons pour maitriser la complexité croissante de ce qu’on appelle un “programme”.

C’est dans ce contexte là de décisions assumées que les Xamarin.Forms se situent. Avec leurs inconvénients connus et minimes et leurs avantages énormes qui justifient le choix de cette solution.

Côté performances les Xamarin.Forms ne posent donc aucun problème, en tout cas pas plus et voire moins que d’utiliser MVVM par exemple…

Cela donne à réfléchir quand on voit les choses sous cet angle !

Mais cela ne veut pas dire qu’il n’y a pas de place à l’optimisation !

Xamarin.Forms et Optimisations

Si les plateformes, les langages, le tooling, les librairies, si tout cela est assumé et choisi avec discernement dans un contexte global où les avantages l’emportent largement il reste un point sur lequel agir : la qualité même du code lui-même, c’est à dire la compétence et le savoir-faire du développeur.

Et bien entendu les Xamarin.Forms n’échappent pas à la règle, il est toujours possible d’optimiser en partant d’un code ne prenant pas en compte cette considération.

Il existe bon nombreux de moyens d’optimiser du code C#, pour le code XAML ceux qui pratiquent WPF depuis longtemps par exemple savent aussi qu’il y a des façons d’écrire un code plus performant que celui qu’on peut produire en première approximation.

Pour être tout à fait pratique voici quelques idées d’optimisation d’un code Xamarin.Forms, donc du code XAML puisque le reste est du C# et que l’optimisation d’un tel code fait depuis des années l’objet d’une multitude d’articles qu’il ne sert à rien de compiler ici.

Mais c’est important de le dire et de le faire remarquer : les Xamarin.Forms ce n’est que la possibilité d’écrire du XAML portable. Le reste est du Xamarin classique (on peut aussi écrire du XForms uniquement C# comme on peut le faire sous WPF, mais XAML est autrement mieux adapté pour décrire une UI).

Accélérer les Apps Android

Voici une astuce peu connue qui ne concerne que les Apps Android mais qui permet de les booster de façon très importante.

Je rappelle au passage qu’une application Xamarin.Forms c’est un projet partagé (une PCL est recommandée mais le mode Share est utilisable) qui produit une DLL et une série de projets natifs (Android, iOS, UWP) qui ne font que faire référence à cette DLL.

Les applications compilées sont bien natives, utilisent un projet spécifique où toutes les particularités de la plateforme sont utilisables. Avec les Xamarin.Forms on intervient très peu sur ces projets mais il est possible de le faire sans décrocher ou s’empêtrer dans des complications savantes.

C’est le cas pour cette optimisation. Elle ne concerne que le projet Android, pas le code commun, n’aura d’effet que sur l’App Android et la modification se fera dans le projet natif Android et non dans le code commun.

La feinte se situe dans l’utilisation de AppCompat.

Au départ ce mécanisme permet à Google d’ajouter la rétro-compatibilité de l’UI (Material Design) aux versions antérieures d’Android pour éviter la fragmentation du look & feel. En utilisant AppCompat on est certain d’avoir le même aspect visuel sur toutes les versions Android même celles qui au départ ne supporte pas Material Design.

C’est déjà en soit une excellente idée puisque l’App sera plus respectueuse de son écosystème. C’est l’avantage des Xamarin.Forms ne pouvoir intervenir à tous les niveaux selon les besoins.

Mais en dehors de cet avantage certain l’utilisation de AppCompat produit un effet de bord intéressant : les UI sont plus fluides et plus rapides. Ce qui est rare quand on ajoute une couche. Mais il s’avère que les fragments Android et autres aspects sont mieux gérés quand AppCompat est activé.

D’où l’idée de s’en servir à la fois pour son but premier mais aussi pour améliorer à moindre frais les performances d’une App Xamarin.Forms !

La façon d’ajouter AppCompat est décrite dans un tutorial de la documentation officielle Xamarin, reportez-vous à celui-ci, aucun intérêt de le recopier ici.

XAML Compilé (XAMLC)

Je le laissais entendre en début d’article XAML est une forme de sérialisation XML. Comme toute sérialisation il y a une perte de temps au runtime à relire et interpréter la ressource. Accessoirement l’un des problèmes de XAML c’est que justement les erreurs ne sont découvertes qu’au runtime.

Xamarin a jouté quelque chose de véritablement décisif dans l’amélioration des performances : le support de XAMLC, le XAML compilé.

Sous cette forme XAML est contrôlé et compilé en IL au moment du Build. On détecte les éventuelles erreurs de frappe en amont plutôt qu’à l’exécution et surtout on dispose d’un code IL compilé qui s’exécute bien plus vite et qui ne nécessite plus la phase de réhydratation depuis un texte XML.

Par défaut XAML n’est pas compilé notamment parce qu’il existe des cas limites où la compatibilité avec XAML non compilé n’est pas parfaite. Ces cas sont très rares mais justifient que par défaut la compilation ne soit pas enclenchée pour garantir la compatibilité la plus grande possible avec du code existant ou avec ces cas limites.

Il est donc nécessaire d’activer la compilation XAML, ce que je vous conseille de faire systématiquement.

Et cela peut être fait de deux façons : soit au coup par coup pour une classe donnée, soit globalement pour tout un assembly. Il est même possible de poser un réglage global sur ce dernier et de décrocher ponctuellement pour une classe donnée.

La bascule s’effectue par un attribut :

[XamlCompilation (XamlCompilationOptions.Compile)]

Il se place soit devant la déclaration d’un assembly (pour toute l’application je vous conseille de le placer dans AssemblyInfo.cs) ou bien ponctuellement devant la classe d’un page XAML. Dans ce dernier cas on peut aller ponctuellement à l’envers de la stratégie globale (refuser le compilation de la classe en particulier même si tout est compilé ou le contraire).

Vous trouverez d’autres informations sur cette compilation XAML dans la doc officielle. On notera que cette optimisation comme d’autres sont discutées dans mon livre sur les Xamarin.Forms (pub gratuite).

ListView et recyclage des items

Les listes sont essentielles. Elles sont présentes dans pour ainsi dire toutes les applications mobiles. Assez souvent elles sont même l’élément d’UI principal au coeur des Apps. Par exemple une liste de notes, de courses, de cotations boursières, de trajets effectués, etc… presque toutes les Apps sont basées sur de telles listes.

Optimiser les ListView est donc essentiel.

Et encore une fois par défaut ce n’est pas forcément le mode le plus rapide qui est sélectionné. Pourquoi ? parce que comme pour le XAML compilé, même s’ils sont rares, il existe des cas limites où l’optimisation est contre-productive. Alors le choix est laissé au développeur, à condition qu’il soit au courant…

Ici le principe de l’optimisation est de cibler les items affichés et de modifier la façon dont ceux qui “entrent” et qui “sortent” de la visibilité sont traités.

Par défaut ListView crée un item, une cellule donc (ViewCell, DataTemplate…) pour chaque élément de la liste source (généralement une ObservableCollection).

S’il y a plusieurs pages d’éléments à gérer cela prend du temps de créer tous ces items et de les maintenir en mémoire.

L’optimisation consiste ici à modifier la stratégie de caching de la liste. Au lieu de retenir en mémoire les items, ce qui est fait par défaut donc, on peut demander à ce que les items (les cellules de la liste et pas les items de liste C# sous-jacente) soient recyclés.

Quand l’utilisateur scrolle, l’item qui sort de la visibilité est réutilisé pour afficher celui qui entre dans la visibilité.

Le gain est important car créer un item avec parfois toute sa mise en page et ses bindings cela prend du temps. En réutilisant les items et en se limitant à mette à jour uniquement ses bindings (pour que les bonnes données soient affichées) la liste gagne un temps considérable tout en économisant beaucoup de mémoire (ce qui fait aussi gagner du temps en stressant moins le garbage collector ou la virtualisation mémoire de l’OS).

Scroller devient immédiatement plus rapide. C’est véritablement l’optimisation phare qu’il faut connaitre.

On notera que pour l’instant IntelliSense ne reconnait pas la propriété impliquée et que même en farfouillant vous ne pourriez pas trouver seul cette optimisation…

Pour plus de détail vous pouvez vous reporter à la documentation officielle.

A noter : les cas limites où cette optimisation peut devenir improductive sont ceux où la cellule établit plus de 15/20 bindings. Dans ce cas le recyclage perd autant ou plus de temps à modifier les bindings qu’à créer une nouvelle cellule. Mais en général dépasser 4/5 binding par item n’est pas une bonne pratique de toute façon.

La chasse aux StackLayout

le StackLayout est un élément d’UI très utilisé notamment pour grouper un label et un switch ou un bouton sur une même ligne par exemple, mais aussi dans de nombreuses mises en page. Il est vrai qu’il est particulièrement pratique. L’équivalent XAML WPF est le StackPanel. Seul le nom est différent le fonctionnement est similaire.

Or un StackLayout a une taille qui dépend de son contenu et s’y adapte. Cela complique beaucoup la mise en page faite par le moteur XAML.

On peut optimiser son code en faisant la chasse aux StackLayout qui ne sont pas indispensables. Dans beaucoup de cas on peut les remplacer par des Grid ou des RelativeLayout. La Grid étant le plus rapide puisqu’elle est prédécoupée en lignes et colonnes dont la taille se calcule facilement (même avec des modes Auto ou Star).

Cette optimisation s’applique bien entendu aux ViewCell des ListView. Evitez les StackLayout la liste sera plus fluide.

Cellules standard vs ViewCell

Encore et toujours à propos de la ListView. Son omniprésence dans les Apps justifie plus que tout autre élément d’UI de se pencher sur ses optimisations.

Lorsqu’on désire mettre en page un élément d’une ListView on peut utiliser une ViewCell, totalement générique et vide par défaut on la remplit par du XAML, des StackLayout, des Images, des Labels, etc. Tout cela avec du binding.

Si faire de la sorte est très pratique dans la phase de mise en place d’une UI (il est facile et rapide de modifier le visuel) il n’est pas forcément conseillé de laisser les choses en l’état pour la production.

Les cellules standard fournies par les Xamarin.Forms sont déjà compilées et optimisées pour de meilleurs performances. Elles ne sont en revanche par forcément adaptées à tous les cas.

Pour une personnalisation plus grande tout en conservant de bonnes performances il est conseillé de créer des types de cellules personnalisés. Ils agiront comme les types standard à peu de différences près.

Les Custom Cells sont décrites dans mon livre, mais on trouve bien entendu des explications (en US) sur le site Xamarin.

DataTemplateSelector

Toujours et encore les listes… Cette fois-ci cela concerne les items trop complexes. Ceux qui utilisent par exemple des triggers pour afficher ou non des images, changer la couleur du fond selon certaines conditions, etc.

Au lieu de farcir la ViewCell d’un code trop complexe il est préférable d’utiliser la stratégie du DataTemplateSelector.

Il s’agit ici de créer des Custom Cells pour chaque grand cas possible puis de créer une classe supportant IDataTemplateSelector. Cette classe devant un DataTemplate qui sera utilisé par l’ItemTemplate de la liste.

C’est du code C#, celui du sélecteur, qui retournera la Custom Cell la mieux adaptée. Cela simplifie énormément les choses et fait gagner encore un peu plus en performances…

On trouvera un exemple de mise en œuvre sur les blogs Xamarin (cliquez ici !).

Optimiser l’utilisation des collections observables (et autres helpers)

Les listes (et oui toujours elles !) sont bindées à des collections. Le plus souvent il s’agit d’ObservableCollection puisqu’elles sont capables de réagir à tout ajout ou suppression d’item ce qui garantit la bonne synchronisation de l’UI avec l’état du ViewModel et des Modèles utilisés.

Lorsqu’on initialise ces collections c’est le fait soit d’une lecture en base de données soit par accès à des données distantes. Tout cela retourne des listes d’instances qu’il faut ajouter à la collection présentée par le ViewModel.

S’il s’agit d’une ObsevableCollection l’ajout d’élément ne peut se faire que par un Add(). Il n’existe en effet pas de AddRange() pour ce type de liste.

Or leur intérêt est justement de déclencher le processus de rafraichissement de l’UI à chaque ajout ou suppression d’item. Ce qui à ce moment particulier du remplissage initial de la liste déclenchera de très nombreuses demandes de rafraichissement à l’UI, en pure perte. Les performances seront dégradées par ces appels inutiles.

Il serait bien plus judicieux de pouvoir bloquer tous les appels lors du chargement de la liste et de ne déclencher qu’une seule demande de rafraichissement une fois tous les items ajoutés.

D’où l’intérêt de disposer d’un AddRange() qui traiterait en bloc l’ajout de toute une suite d’item.

Mais cela n’existe pas comme je le disais. Alors pourquoi en parler ?

Parce que James Montemagno qui a écrit de nombreuses librairies annexes ou démos pour les Xamarin.Forms (il travaille chez Xamarin) a produit entre autres choses une librairie s’appelant “mvvm-helpers”.

Et dans cette librairie on découvre une ObservableCollection qui possède un AddRange(), et ce justement pour les raisons expliquées ici…

On trouver aussi dans cette petite lib bien pratique un ViewModelBase ou un mécanisme asynchrone de timeout applicable à toute opération pouvant dépasser un temps limite. Il y a aussi quelques facilités proposées pour gérer le groupage des items d’une liste ce qui est très utile pour afficher une classement par ordre de l’alphabet ou bénéficier automatiquement du zoom sémantique sous UWP (ce que je décris dans mon livre, c’est incroyable non? !).

Les Custom Renderers

Evoqués dans cet article les Renderers sont ces morceaux de code qui permettent à Xamarin.Forms de transformer les classes portables utilisées par le développeur (ListView, Button, StackLayout…) en classes natives de chaque plateforme.

Au lieu d’être un procédé secret, fermé, opaque, Xamarin en a fait un mécanisme ouvert et accessible.

Il est donc possible d’écrire des Custom Renderers qu’on enregistre dans le système IoC des Xamarin.Forms. Ils seront utilisés à la place des Renderers de base.

Cette astuce est essentielle dans certains cas puisqu’elle vous permettra sans décrocher des XForms de fournir pour telle ou telle plateforme un contrôle ultra optimisé et totalement natif.

Performances, personnalisations de l’UI, respect de la charte graphique de chaque plateforme, le tout en manipulant toujours un code central unique et portable…

La mise en œuvre côté Xamarin.Forms n’est pas compliquée (on fournit une classe qu’on enregistre), en revanche le moindre exemple réclame d’entrer dans le code d’UI natif de chaque plateforme ce qui est forcément plus long et plus technique.

On trouvera toute une série d’articles sur cette question sur le site officiel Xamarin. C’est un sujet que j’aborde dans mon livre mais que je creuserai certainement plus dans de prochains articles ici.

Conclusion

On peut être en natif cela ne supprime en aucun cas le besoin d’être bien formé pour être capable d’effectuer des optimisations du code.

L’optimisation du code C# est un sujet débattu depuis des années et pour lequel on trouve largement matière sur le Web. En revanche le savoir-faire XAML est plutôt une connaissance d’expert qui se garde jalousement pour justifier du prix de ses interventions ou de son salaire…

Je n’ai jamais fonctionné comme cela. Ma différence (comme la vôtre) c’est mon expérience et mon intelligence, pas ce que je sais et que je garderai secret. Avec les mêmes informations deux personnes n’obtiendront pas le même résultat car elles pensent de façon différente (sans même parler d’intelligence ou d’expérience plus ou moins grande, juste d’être différent).

Mon unicité comme la vôtre n’est pas dans ce que nous connaissons mais dans ce que nous en faisons.

Il n’y a donc pas de secrets à garder. Tout avantage qui ne tient qu’à des secrets s’écroule un jour. Celui qui se fonde sur l’apport de l’originalité de chacun ne risque rien à dévoiler les astuces du métier.

C’est pour cela que Dot.Blog est si riche, je ne cache rien Sourire

Mais si vous avez besoin d’un expert pour une formation ou un développement, vous apprécierez “ma” différence…

Sur ce, optimisez bien votre code XAML et vous obtiendrez des applications Xamarin.Forms fluides et rapides comme l’éclair !

Stay Tuned !

Faites des heureux, partagez l'article !
blog comments powered by Disqus