it-roy-ru.com

Почему Xamarin.Forms такие медленные при отображении нескольких меток (особенно на Android)?

Мы пытаемся выпустить некоторые продуктивные приложения с Xamarin.Forms, но одна из наших главных проблем - общая медлительность между нажатием кнопки и отображением контента. После нескольких экспериментов мы обнаружили, что даже простая ContentPage с 40 метками требует более 100 мсек:

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

Особенно на Android все хуже, чем больше ярлыков вы пытаетесь отобразить. Первое нажатие кнопки (что крайне важно для пользователя) даже занимает ~ 300 мс. Нам нужно показать что-то на экране менее чем за 30 мс, чтобы создать хороший пользовательский опыт.

Почему Xamarin.Forms занимает так много времени, чтобы отобразить несколько простых ярлыков? И как обойти эту проблему, чтобы создать приложение для отправки?

Эксперименты

Код может быть разветвлен на GitHub по адресу https://github.com/perpetual-mobile/XFormsPerformance

Я также написал небольшой пример, чтобы продемонстрировать, что подобный код, использующий нативные API-интерфейсы из Xamarin.Android, значительно быстрее и не замедляется при добавлении дополнительного содержимого: https://github.com/perpetual-mobile/XFormsPerformance/ дерево/Android-native-api

14
Rodja

Команда поддержки Xamarin написала мне:

Команда знает об этой проблеме, и они работают над оптимизацией Код инициализации пользовательского интерфейса. Вы можете увидеть некоторые улучшения в предстоящем релизы.

Обновление: после семи месяцев простоя Xamarin изменил статус отчет об ошибке на «ПОДТВЕРЖДЕН».

Хорошо знать. Поэтому мы должны быть терпеливыми. К счастью, Шон Маккей на форумах Xamarin предложил переопределить весь код верстки для повышения производительности: https://forums.xamarin.com/discussion/comment/87393#Comment_87393

Но его предложение также означает, что мы должны снова написать полный код этикетки. Вот версия FixedLabel, которая не выполняет дорогостоящих циклов компоновки и имеет некоторые функции, такие как привязываемые свойства для текста и цвета. Использование этого параметра вместо Label повышает производительность на 80% и более в зависимости от количества меток и места их появления.

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

Android Renderer выглядит так:

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

И здесь 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

То, что вы измеряете здесь, является суммой:

  • время, необходимое для создания страницы
  • планировать это
  • оживить его на экране

На nexus5 я наблюдаю времена в 300 мс для первого звонка и 120 мс для последующих.

Это связано с тем, что OnAppearing () будет вызываться только тогда, когда представление полностью анимировано на месте.

Вы можете легко измерить время анимации, заменив ваше приложение на:

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

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

и я наблюдаю времена как:

134.045 ms
2.796 ms
3.554 ms

Это дает некоторые идеи: - нет никакой анимации на PushAsync на Android (есть на iPhone, занимает 500 мс) - при первом нажатии на страницу вы платите налог в размере 120 мс из-за нового распределения . - XF делает хорошую работу по повторному использованию средств визуализации страниц, если это возможно

Что нас интересует, так это время показа 40 этикеток, ничего больше. Давайте снова изменим код:

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

наблюдаемое время (на 3 звонка):

264.015 ms
186.772 ms
189.965 ms
188.696 ms

Это все еще слишком много, но поскольку ContentView установлен первым, он измеряет 40 циклов макета, поскольку каждая новая метка перерисовывает экран. Давайте изменим это:

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

И вот мои измерения:

178.685 ms
110.221 ms
117.832 ms
117.072 ms

Это становится очень разумным, особенно учитывая, что вы рисуете (создаете и измеряете) 40 меток, когда на экране может отображаться только 20.

Действительно, есть еще возможности для улучшения, но ситуация не так плоха, как кажется. Правило 30 мс для мобильных устройств гласит, что все, что занимает более 30 мс, должно быть асинхронным, а не блокировать пользовательский интерфейс. Здесь для переключения страницы требуется чуть больше 30 мс, но с точки зрения пользователя, я не воспринимаю это как медленную. 

4
Stephane Delcroix

В установщиках свойств Text и TextColor класса FixedLabel код говорит:

Если новое значение «отличается» от текущего, то ничего не делать! 

Это должно быть с противоположным условием, так что если новое значение то же самое , что и текущее значение, то делать нечего.

1
Aspro