web-dev-qa-db-ger.com

Warum ist Xamarin.Forms so langsam, wenn einige Labels angezeigt werden (insbesondere unter Android)?

Wir versuchen, einige produktive Apps mit Xamarin.Forms zu veröffentlichen, aber eines unserer Hauptprobleme ist die allgemeine Langsamkeit zwischen dem Drücken von Tasten und dem Anzeigen von Inhalten. Nach einigen Experimenten stellten wir fest, dass selbst eine einfache ContentPage mit 40 Bezeichnungen mehr als 100 ms benötigt, um angezeigt zu werden:

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();
    }
}

Besonders auf Android wird es immer schlimmer, je mehr Labels Sie anzeigen möchten. Der erste Tastendruck (der für den Benutzer entscheidend ist) dauert sogar ~ 300 ms. Wir müssen in weniger als 30 ms etwas auf dem Bildschirm anzeigen, um eine gute Benutzererfahrung zu erzielen.

Warum dauert es mit Xamarin.Forms so lange, bis einige einfache Bezeichnungen angezeigt werden? Und wie kann man dieses Problem umgehen, um eine versandfähige App zu erstellen?

Experimente

Der Code kann auf GitHub unter https://github.com/perpetual-mobile/XFormsPerformance gespalten werden.

Ich habe auch ein kleines Beispiel geschrieben, um zu demonstrieren, dass ähnlicher Code unter Verwendung der nativen APIs von Xamarin.Android erheblich schneller ist und nicht langsamer wird, wenn mehr Inhalte hinzugefügt werden: https://github.com/perpetual-mobile/XFormsPerformance/ Baum/Android-native-api

14
Rodja

Das Xamarin Support Team schrieb mir:

Das Team ist sich des Problems bewusst und arbeitet an der Optimierung des Initialisierungscodes der - Benutzeroberfläche. In den kommenden Releases werden möglicherweise einige Verbesserungen angezeigt.

Update: Nach siebenmonatigem Leerlauf hat Xamarin den Status Bug Report in 'CONFIRMED' geändert.

Gut zu wissen. Wir müssen also geduldig sein. Zum Glück schlug Sean McKay in den Xamarin-Foren vor, den gesamten Layout-Code zur Verbesserung der Leistung zu überschreiben: https://forums.xamarin.com/discussion/comment/87393#Comment_87393

Sein Vorschlag bedeutet aber auch, dass wir den kompletten Labelcode erneut schreiben müssen. Hier ist eine Version eines FixedLabels, die nicht die kostspieligen Layoutzyklen durchführt und einige Funktionen wie bindbare Eigenschaften für Text und Farbe aufweist. Die Verwendung dieser Option anstelle von Label verbessert die Leistung um 80% und mehr, je nach Anzahl der Labels und wo diese auftreten.

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));
    }
}

Der Android-Renderer sieht folgendermaßen aus:

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;
        }
    }
}

Und hier das iOS Render:

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;
        }
    }
}
15
Rodja

Was Sie hier messen, ist die Summe aus:

  • die Zeit, die zum Erstellen der Seite benötigt wird
  • um es zu gestalten
  • um es auf dem Bildschirm zu animieren

Auf einem nexus5 beobachte ich für den ersten Anruf Zeiten in der Größenordnung von 300 ms und für nachfolgende Aufrufe von 120 ms.

Dies liegt daran, dass OnAppearing () nur aufgerufen wird, wenn die Ansicht vollständig animiert ist.

Sie können die Animationszeit leicht messen, indem Sie Ihre App durch Folgendes ersetzen:

public class StopPage : ContentPage
{
    public StopPage()
    {
    }

    protected override void OnAppearing()
    {
        System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");        
        base.OnAppearing();
    }
}

und ich beobachte mal wie:

134.045 ms
2.796 ms
3.554 ms

Dies gibt einige Einblicke: - Auf PushAsync gibt es keine Animationen auf Android (es gibt auf dem iPhone 500ms). - Wenn Sie zum ersten Mal die Seite drücken, zahlen Sie eine 120ms-Steuer aufgrund einer neuen Allocation. - XF macht gute Arbeit, wenn es darum geht, Seitenrenderer wiederzuverwenden

Was uns interessiert, ist die Zeit für die Anzeige der 40 Etiketten, sonst nichts. Lassen Sie uns den Code noch einmal ändern:

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();
    }
}

mal beobachtet (bei 3 anrufen):

264.015 ms
186.772 ms
189.965 ms
188.696 ms

Das ist immer noch ein bisschen zu viel, aber da ContentView zuerst eingestellt wird, werden 40 Layoutzyklen gemessen, da jedes neue Label den Bildschirm neu zeichnet. Ändern wir das:

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();
    }
}

Und hier sind meine Maße:

178.685 ms
110.221 ms
117.832 ms
117.072 ms

Dies wird sehr vernünftig, insbesondere. Wenn Sie 40 Beschriftungen zeichnen (instanziieren und messen), kann Ihr Bildschirm nur 20 anzeigen.

Es gibt zwar noch Verbesserungsbedarf, aber die Situation ist nicht so schlimm, wie es scheint. Die 30-ms-Regel für Mobilgeräte besagt, dass alles, was mehr als 30 ms dauert, asynchron sein sollte und die Benutzeroberfläche nicht blockiert. Es dauert zwar etwas mehr als 30 ms, um eine Seite zu wechseln, aber aus Anwendersicht empfinde ich dies nicht als langsam. 

4

In den Setters der Text- und TextColor-Eigenschaften der FixedLabel-Klasse lautet der Code:

Wenn der neue Wert vom aktuellen Wert "abweicht", dann nichts tun! 

Es sollte mit der entgegengesetzten Bedingung sein, so dass, wenn der neue Wert der gleiche wie der aktuelle Wert ist, nichts zu tun ist.

1
Aspro