为什么Xamarin.Forms在显示一些标签时会如此慢(特别是在Android上)?

时间:2014-10-14 15:24:31

标签: c# android performance xamarin xamarin.forms

我们正在尝试使用Xamarin.Forms发布一些高效的应用程序,但我们的主要问题之一是按钮按下和显示内容之间的总体缓慢。经过一些实验,我们发现即使是一个带有40个标签的简单ContentPage也需要超过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显示一些简单标签需要这么长时间?如何解决这个问题来创建一个可交付的应用程序?

实验

代码可以在https://github.com/perpetual-mobile/XFormsPerformance

的GitHub上分叉

我还写了一个小例子来证明使用Xamarin.Android中的原生API的类似代码明显更快,并且在添加更多内容时不会变慢:https://github.com/perpetual-mobile/XFormsPerformance/tree/android-native-api

3 个答案:

答案 0 :(得分:15)

Xamarin支持团队写信给我:

  

团队意识到了这个问题,他们正在努力优化问题   UI初始化代码。你可能会看到即将到来的一些改进   版本。

更新:在闲置七个月后,Xamarin将bug report状态更改为'确认'。

很高兴知道。所以我们要耐心等待。幸运的是,Sean McKay在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渲染:

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

答案 1 :(得分:4)

您在这里测量的是以下总和:

  • 创建页面所需的时间
  • 布局
  • 在屏幕上制作动画

在nexus5上,我观察到第一次呼叫的时间为300ms,后续时间为120ms。

这是因为仅在视图完全动画处理时才会调用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

这提供了一些见解:   - 在Android上的PushAsync上没有动画(在iPhone上有这个&#39; s,需要500ms)   - 第一次推送页面时,由于新的分配,您需要支付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

这变得非常合理,尤其是因为当你的屏幕只能显示20个标签时,你正在绘制(实例化和测量)40个标签。

确实还有一些改进空间,但情况并不像看起来那么糟糕。移动设备的30ms规则表明,超过30毫秒的所有内容都应该是异步的,而不是阻止UI。这里切换页面需要30多分钟,但从用户的角度来看,我并不觉得这很慢。

答案 2 :(得分:1)

Text类的TextColorFixedLabel属性的setter中,代码显示:

  

如果新值与当前值“不同”,则不执行任何操作!

它应该是相反的条件,所以如果新值与当前值相同,那么就没有任何事情要做。