Nous essayons de publier des applications productives avec Xamarin.Forms, mais l’un de nos principaux problèmes est la lenteur générale entre l’appui sur les boutons et l’affichage du contenu. Après quelques expériences, nous avons découvert que même une simple ContentPage
avec 40 étiquettes prend plus de 100 ms pour apparaître:
public static class App
{
public static DateTime StartTime;
public static Page GetMainPage()
{
return new NavigationPage(new StartPage());
}
}
public class StartPage : ContentPage
{
public StartPage()
{
Content = new Button {
Text = "Start",
Command = new Command(o => {
App.StartTime = DateTime.Now;
Navigation.PushAsync(new StopPage());
}),
};
}
}
public class StopPage : ContentPage
{
public StopPage()
{
Content = new StackLayout();
for (var i = 0; i < 40; i++)
(Content as StackLayout).Children.Add(new Label{ Text = "Label " + i });
}
protected override void OnAppearing()
{
((Content as StackLayout).Children[0] as Label).Text = "Stop after " + (DateTime.Now - App.StartTime).TotalMilliseconds + " ms";
base.OnAppearing();
}
}
Surtout sur Android, le nombre d'étiquettes que vous essayez d'afficher est de pire en pire. La première pression sur un bouton (ce qui est crucial pour l'utilisateur) prend même environ 300 ms. Nous devons afficher quelque chose à l'écran en moins de 30 ms pour créer une bonne expérience utilisateur.
Pourquoi est-ce si long avec Xamarin.Forms
d'afficher des étiquettes simples? Et comment contourner ce problème pour créer une application pouvant être expédiée?
Expériences
Le code peut être créé dans GitHub à l’adresse https://github.com/perpetual-mobile/XFormsPerformance
J'ai également écrit un petit exemple pour démontrer qu'un code similaire utilisant les API natives de Xamarin.Android est nettement plus rapide et ne ralentit pas lors de l'ajout de contenu: https://github.com/perpetual-mobile/XFormsPerformance/ arbre/Android-native-api
L'équipe d'assistance Xamarin m'a écrit:
L’équipe est consciente du problème et s’emploie à optimiser le fichier Code d'initialisation de l'interface utilisateur. Vous pouvez voir des améliorations dans les prochains communiqués.
Mise à jour: après sept mois d'inactivité, Xamarin a modifié le rapport de bogue status sur 'CONFIRMED'.
Bon à savoir. Nous devons donc être patients. Heureusement, Sean McKay sur Xamarin Les forums ont suggéré de remplacer tout le code de mise en page pour améliorer les performances: https://forums.xamarin.com/discussion/comment/87393#Comment_87393
Mais sa suggestion signifie également que nous devons réécrire le code complet de l'étiquette. Voici une version d'un FixedLabel qui ne fait pas les cycles de mise en page coûteux et possède certaines fonctionnalités telles que les propriétés de liaison pour le texte et la couleur. L'utilisation de ceci au lieu de Label
améliore les performances de 80% et plus en fonction du nombre d'étiquettes et de l'endroit où elles se produisent.
public class FixedLabel : View
{
public static readonly BindableProperty TextProperty = BindableProperty.Create<FixedLabel,string>(p => p.Text, "");
public static readonly BindableProperty TextColorProperty = BindableProperty.Create<FixedLabel,Color>(p => p.TextColor, Style.TextColor);
public readonly double FixedWidth;
public readonly double FixedHeight;
public Font Font;
public LineBreakMode LineBreakMode = LineBreakMode.WordWrap;
public TextAlignment XAlign;
public TextAlignment YAlign;
public FixedLabel(string text, double width, double height)
{
SetValue(TextProperty, text);
FixedWidth = width;
FixedHeight = height;
}
public Color TextColor {
get {
return (Color)GetValue(TextColorProperty);
}
set {
if (TextColor != value)
return;
SetValue(TextColorProperty, value);
OnPropertyChanged("TextColor");
}
}
public string Text {
get {
return (string)GetValue(TextProperty);
}
set {
if (Text != value)
return;
SetValue(TextProperty, value);
OnPropertyChanged("Text");
}
}
protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
{
return new SizeRequest(new Size(FixedWidth, FixedHeight));
}
}
Le Renderer Android ressemble à ceci:
public class FixedLabelRenderer : ViewRenderer
{
public TextView TextView;
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e)
{
base.OnElementChanged(e);
var label = Element as FixedLabel;
TextView = new TextView(Context);
TextView.Text = label.Text;
TextView.TextSize = (float)label.Font.FontSize;
TextView.Gravity = ConvertXAlignment(label.XAlign) | ConvertYAlignment(label.YAlign);
TextView.SetSingleLine(label.LineBreakMode != LineBreakMode.WordWrap);
if (label.LineBreakMode == LineBreakMode.TailTruncation)
TextView.Ellipsize = Android.Text.TextUtils.TruncateAt.End;
SetNativeControl(TextView);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Text")
TextView.Text = (Element as FixedLabel).Text;
base.OnElementPropertyChanged(sender, e);
}
static GravityFlags ConvertXAlignment(Xamarin.Forms.TextAlignment xAlign)
{
switch (xAlign) {
case Xamarin.Forms.TextAlignment.Center:
return GravityFlags.CenterHorizontal;
case Xamarin.Forms.TextAlignment.End:
return GravityFlags.End;
default:
return GravityFlags.Start;
}
}
static GravityFlags ConvertYAlignment(Xamarin.Forms.TextAlignment yAlign)
{
switch (yAlign) {
case Xamarin.Forms.TextAlignment.Center:
return GravityFlags.CenterVertical;
case Xamarin.Forms.TextAlignment.End:
return GravityFlags.Bottom;
default:
return GravityFlags.Top;
}
}
}
Et voici le rendu iOS:
public class FixedLabelRenderer : ViewRenderer<FixedLabel, UILabel>
{
protected override void OnElementChanged(ElementChangedEventArgs<FixedLabel> e)
{
base.OnElementChanged(e);
SetNativeControl(new UILabel(RectangleF.Empty) {
BackgroundColor = Element.BackgroundColor.ToUIColor(),
AttributedText = ((FormattedString)Element.Text).ToAttributed(Element.Font, Element.TextColor),
LineBreakMode = ConvertLineBreakMode(Element.LineBreakMode),
TextAlignment = ConvertAlignment(Element.XAlign),
Lines = 0,
});
BackgroundColor = Element.BackgroundColor.ToUIColor();
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Text")
Control.AttributedText = ((FormattedString)Element.Text).ToAttributed(Element.Font, Element.TextColor);
base.OnElementPropertyChanged(sender, e);
}
// copied from iOS LabelRenderer
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (Control == null)
return;
Control.SizeToFit();
var num = Math.Min(Bounds.Height, Control.Bounds.Height);
var y = 0f;
switch (Element.YAlign) {
case TextAlignment.Start:
y = 0;
break;
case TextAlignment.Center:
y = (float)(Element.FixedHeight / 2 - (double)(num / 2));
break;
case TextAlignment.End:
y = (float)(Element.FixedHeight - (double)num);
break;
}
Control.Frame = new RectangleF(0, y, (float)Element.FixedWidth, num);
}
static UILineBreakMode ConvertLineBreakMode(LineBreakMode lineBreakMode)
{
switch (lineBreakMode) {
case LineBreakMode.TailTruncation:
return UILineBreakMode.TailTruncation;
case LineBreakMode.WordWrap:
return UILineBreakMode.WordWrap;
default:
return UILineBreakMode.Clip;
}
}
static UITextAlignment ConvertAlignment(TextAlignment xAlign)
{
switch (xAlign) {
case TextAlignment.Start:
return UITextAlignment.Left;
case TextAlignment.End:
return UITextAlignment.Right;
default:
return UITextAlignment.Center;
}
}
}
Ce que vous mesurez ici est la somme de:
Sur un nexus5, j’observe des délais de l'ordre de 300 ms pour le premier appel et de 120 ms pour les suivants.
Cela est dû au fait que la méthode OnAppearing () ne sera invoquée que lorsque la vue est entièrement animée sur place.
Vous pouvez facilement mesurer le temps d'animation en remplaçant votre application par:
public class StopPage : ContentPage
{
public StopPage()
{
}
protected override void OnAppearing()
{
System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");
base.OnAppearing();
}
}
et j'observe des temps comme:
134.045 ms
2.796 ms
3.554 ms
Cela donne quelques indications: - Il n'y a pas d'animation sur PushAsync sur Android (il y a sur iPhone, prenant 500 ms) - la première fois que vous appuyez sur la page, vous payez une taxe de 120ms, en raison d'une nouvelle attribution . - XF réussit si bien à réutiliser les rendus de page
Ce qui nous intéresse, c'est le temps d'affichage des 40 étiquettes, rien d'autre. Changeons encore le code:
public class StopPage : ContentPage
{
public StopPage()
{
}
protected override void OnAppearing()
{
App.StartTime = DateTime.Now;
Content = new StackLayout();
for (var i = 0; i < 40; i++)
(Content as StackLayout).Children.Add(new Label{ Text = "Label " + i });
System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");
base.OnAppearing();
}
}
fois observés (sur 3 appels):
264.015 ms
186.772 ms
189.965 ms
188.696 ms
C'est encore un peu trop, mais comme ContentView est configuré en premier, il mesure 40 cycles de mise en page, chaque nouvelle étiquette redessinant l'écran. Changeons cela:
public class StopPage : ContentPage
{
public StopPage()
{
}
protected override void OnAppearing()
{
App.StartTime = DateTime.Now;
var layout = new StackLayout();
for (var i = 0; i < 40; i++)
layout.Children.Add(new Label{ Text = "Label " + i });
Content = layout;
System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");
base.OnAppearing();
}
}
Et voici mes mensurations:
178.685 ms
110.221 ms
117.832 ms
117.072 ms
Cela devient très raisonnable, esp. étant donné que vous dessinez (instanciez et mesurez) 40 étiquettes lorsque votre écran ne peut en afficher que 20.
Il y a certes encore des progrès à faire, mais la situation n'est pas aussi grave qu'il y paraît. La règle des 30 ms pour mobile indique que tout ce qui prend plus de 30 ms doit être asynchrone et non bloquer l'interface utilisateur. Ici, il faut un peu plus de 30 ms pour changer de page, mais du point de vue de l'utilisateur, cela ne me semble pas lent.
Dans les setters des propriétés Text
et TextColor
de la classe FixedLabel
, le code dit:
Si la nouvelle valeur est "différente" de la valeur actuelle, ne faites rien!
Ce devrait être avec la condition opposée, de sorte que si la nouvelle valeur est le même que la valeur actuelle, il n'y a rien à faire.