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

我们正在尝试用Xamarin发布一些有生产力的应用程序。但是我们主要的问题之一就是按下button和显示内容之间的整体缓慢。 经过几次实验,我们发现,即使是一个简单的带有40个标签的ContentPage需要100多ms的时间才能显示出来:

 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上,你试图显示的标签越多越糟糕。 第一个button(对用户来说至关重要)甚至需要约300毫秒。 我们需要在不到30毫秒的时间内在屏幕上显示某些内容,以创build良好的用户体验。

为什么Xamarin.Forms需要这么长时间才能显示一些简单的标签? 以及如何解决这个问题,以创build一个shippable应用程序?

实验

代码可以在Github上分叉https://github.com/perpetual-mobile/XFormsPerformance

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

Solutions Collecting From Web of "为什么Xamarin.Forms显示一些标签(特别是在Android上)很慢?"

Xamarin支持团队写道:

团队意识到这个问题,他们正在优化UI初始化代码。 即将发布的版本可能会有所改进。

更新:Xamarin在空闲七个月之后将错误报告状态更改为“确认”。

很高兴知道。 所以我们必须要有耐心。 幸运的是,Xamarin论坛中的Sean McKaybuild议覆盖所有的布局代码以提高性能: https : //forums.xamarin.com/discussion/comment/87393#Comment_87393

但是他的build议也意味着我们必须重新编写完整的标签代码。 这是一个FixedLabel的版本,它不会花费昂贵的布局周期,并具有一些像文本和颜色的可绑定属性的function。 使用这个而不是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; } } } 

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

  • 创build页面所需的时间
  • 来布置它
  • 在屏幕上animation

在一个连接点上,我观察了第一次通话300ms的时间和后面的120ms的次数。

这是因为OnAppearing()只会在视图完全animation的时候被调用。

您可以通过replace您的应用程序来轻松测量animation时间:

 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上没有animation(在iPhone上有500ms的时间) – 第一次按下这个页面时,由于新的分配,你需要支付120ms的税。 – 如果可能,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。

确实还有一些改进的空间,但情况并不像看起来那么糟糕。 移动30ms规则说,超过30ms的所有应该是asynchronous的,不会阻止用户界面。 这里需要30ms以上的时间来切换一个页面,但是从用户的angular度来看,我并不觉得这样慢。

FixedLabel类的TextTextColor属性的setter中,代码说:

如果新值与当前值不同,则不做任何事情!

它应该是相反的条件,所以如果新的价值和现在的一样 ,那就没有什么可做的了。