Xamarin.Formsを使用して生産的なアプリをリリースしようとしていますが、主な問題の1つは、ボタンを押してからコンテンツが表示されるまでの全体的な遅延です。数回の実験の結果、ラベルが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
Xamarinサポートチームが私に書いた:
チームはこの問題を認識しており、UI初期化コードの最適化に取り組んでいます。今後のリリースでいくつかの改善が見られるかもしれません。
更新:7か月のアイドリングの後、Xamarinは バグレポート ステータスを「確認済み」に変更しました。
知っておくと良い。だから我慢する必要がある。幸い、XamarinフォーラムでSean McKayがパフォーマンスを改善するためにすべてのレイアウトコードをオーバーライドすることを提案しました: https://forums.xamarin.com/discussion/comment/87393#Comment_8739
しかし、彼の提案は、完全なラベルコードを再度記述する必要があることも意味します。これは、コストのかかるレイアウトサイクルを行わず、テキストと色のバインド可能なプロパティなどのいくつかの機能を持つ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;
}
}
}
ここで測定しているのは、次の合計です。
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(iPhoneには500msかかります)のPushAsyncにはアニメーションがありません)-ページを初めてプッシュしたとき、新しい割り当てにより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
これは非常に合理的になりつつあります。画面に20しか表示できないときに、40のラベルを描画(インスタンス化、測定)しているとします。
確かにまだ改善の余地はありますが、状況は見た目ほど悪くはありません。モバイルの30ミリ秒ルールでは、30ミリ秒以上かかるものはすべて非同期で、UIをブロックしないようにする必要があるとしています。ここでは、ページを切り替えるのに30ミリ秒以上かかりますが、ユーザーの観点からは、これが遅いとは思いません。
Text
クラスのTextColor
およびFixedLabel
プロパティのセッターで、コードは次のように述べています。
新しい値が現在の値と「異なる」場合は、何もしないでください。
新しい値が現在の値と同じ同じの場合、何もする必要がないように、これは逆の条件である必要があります。