为什么Xamarin.Forms在显示少量标签时非常缓慢(特别是在Android上)?

16

我们正试图使用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 显示一些简单标签需要这么长时间?如何解决这个问题,创建可发布的应用程序?

实验

代码可以在 GitHub 上进行分支管理:https://github.com/perpetual-mobile/XFormsPerformance

我还编写了一个小示例,以证明利用来自 Xamarin.Android 的本地API的类似代码显着更快且不会在添加更多内容时变慢:https://github.com/perpetual-mobile/XFormsPerformance/tree/android-native-api


1
Xamarin支持团队创建了一个Bugzilla问题:https://bugzilla.xamarin.com/show_bug.cgi?id=23822 - Rodja
3个回答

15

Xamarin支持团队给我写信:

该团队已经意识到这个问题,并正在努力优化UI初始化代码。您可能会在即将推出的版本中看到一些改进。

更新:七个月的空闲后,Xamarin将错误报告状态更改为“CONFIRMED”。

好消息是我们需要耐心等待。幸运的是,Xamarin论坛的Sean McKay建议覆盖所有布局代码以提高性能: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渲染器长这样:

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

但是你如何知道视图级别的标签尺寸?我的意思是它们根据本地视图和字体等因素而异。 - Miha Markic
1
我们经常使用http://xforms-kickstarter.com/#referring-to-the-screen-size来确定某些东西应该有多少空间。有时需要使用Device.OnPlatform来考虑平台特定性。总的来说,这不是一个很好的解决方案。这是一个肮脏的变通方法,使得我们可以在客户项目中不使用Xamarin.Forms。 - Rodja
这个漏洞在今年早些时候被解决了。你测试过性能是否更可接受了吗? - BlueRaja - Danny Pflughoeft

4
你所测量的是以下内容的总和:
  • 创建页面所需的时间
  • 对其进行布局所需的时间
  • 在屏幕上执行动画所需的时间
在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

这些内容有一些见解: - Android上的PushAsync没有动画(iPhone上有,需要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规则指出,所有需要超过30ms的操作都应该是异步的,而不是阻塞用户界面。在这里,切换页面需要略多于30ms,但从用户的角度来看,我并不认为这很慢。


2
为了对交互性能进行陈述,您需要测量从单击事件触发到内容出现在屏幕上所需的时间。在“OnAppearing”中启动计时器已经太晚了。 - Rodja
是的,当将视图添加到已经可见的布局中时,Xamarin.Forms会进行一些相当昂贵的测量(我猜这是问题的一部分)。这就是为什么我在OnAppearing之前添加了所有内容。我认为Xamarin.Forms应该在一个步骤中进行布局测量以节省时间。 - Rodja
尝试使用不同数量的标签。一个标签时速度相当快(约10毫秒)。添加的标签越多,速度就越慢。我编写了一个小例子,使用Xamarin.Android在https://github.com/perpetual-mobile/XFormsPerformance/tree/android-native-api上展示了使用更多标签时没有性能损失的情况。请记住:这只是一个最小的例子。其他视图和布局在使用Xamarin.Forms时也会增加缓慢。这是不合理的! - Rodja
我已经使用不同数量的标签进行了测试,结果确实是线性的,我知道。 - Stephane Delcroix
7
Stephane,Xamarin 支持团队告诉我您是 Xamarin.Forms 开发人员之一。我建议您更新您的答案以包含该事实。您的答案真的是官方关于这个问题的观点吗?您个人负责处理性能问题吗?Xamarin 是否计划解决不能编写与原生 API 写的应用程序类似性能的 Xamarin.Forms 应用程序的问题? - Rodja
7
我完全同意@Rodja的观点。Xamarin应该警告用户,XForms并不适合生产应用程序。我们的应用程序遇到了很多问题,联系了Xamarin支持后,他们只回答说Xamarin.Forms尚不适用于实际应用程序。他们私下推荐使用Xamarin.IOS和Xamarin.Droid进行开发。太糟糕了……如果他们对XForms的性能更加诚实,我们本可以节省很多开发时间。 - I.G. Pascual

2
FixedLabel类的TextTextColor属性的设置器中,代码如下:

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

应该相反,即如果新值与当前值相同,则无需进行任何操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接